plan-governance-cli 2.0.1 → 3.0.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/README.md +20 -8
- package/bin/plan-governance-cli.mjs +58 -20
- package/package.json +1 -1
- package/resources/skill/SKILL.md +3 -1
- package/resources/skill/assets/plan.template.md +4 -2
- package/resources/skill/references/cli.md +25 -10
- package/resources/skill/references/planning.md +3 -1
- package/resources/skill/references/verification.md +14 -12
- package/scripts/check_plan_governance.py +539 -108
- package/scripts/graph_governance.mjs +67 -95
- package/scripts/init_plan_governance.py +81 -14
- package/scripts/plan_governance_hook.py +72 -27
|
@@ -231,6 +231,17 @@ def legacy_review_method(kind):
|
|
|
231
231
|
return "自验" if kind == "复核基线复用" or "自验" in kind else "独立"
|
|
232
232
|
|
|
233
233
|
|
|
234
|
+
def independent_review_requested(summary):
|
|
235
|
+
"""只有明确的用户请求证据才将待办派给独立复核。"""
|
|
236
|
+
request = summary.get("独立复核请求", "").strip()
|
|
237
|
+
return bool(request and request != "无" and not is_placeholder(request))
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def review_unavailable(conclusion):
|
|
241
|
+
"""工具未完成不等于审出了实质问题。"""
|
|
242
|
+
return conclusion.strip().startswith(("超时", "复核工具超时", "复核入口不可用"))
|
|
243
|
+
|
|
244
|
+
|
|
234
245
|
def phase_precedes(plan_text, candidate, current):
|
|
235
246
|
"""只允许路线图中更早的阶段;标准数字阶段可在精简路线图中回退比较。"""
|
|
236
247
|
roadmap = [row[0] for row in phase_roadmap_rows(plan_text)]
|
|
@@ -243,7 +254,7 @@ def phase_precedes(plan_text, candidate, current):
|
|
|
243
254
|
|
|
244
255
|
|
|
245
256
|
def legacy_review_state(plan_text, phase):
|
|
246
|
-
"""
|
|
257
|
+
"""在不改写六列历史的前提下应用默认自验和真实发现闭环。"""
|
|
247
258
|
result = {"passed": False, "pending_action": None, "issues": [], "blockers": []}
|
|
248
259
|
issues, blockers = result["issues"], result["blockers"]
|
|
249
260
|
rows = review_history_rows(plan_text)
|
|
@@ -299,7 +310,7 @@ def legacy_review_state(plan_text, phase):
|
|
|
299
310
|
outcome = review_outcome(row[3])
|
|
300
311
|
if method == "独立":
|
|
301
312
|
state["reviewed"] = completed_independent(row)
|
|
302
|
-
if outcome == "failed":
|
|
313
|
+
if outcome == "failed" and not review_unavailable(row[3]):
|
|
303
314
|
state["unresolved"] = True
|
|
304
315
|
elif (row[1] == "修复自验" and state["reviewed"] and outcome == "passed"
|
|
305
316
|
and qualified(row)):
|
|
@@ -319,7 +330,7 @@ def legacy_review_state(plan_text, phase):
|
|
|
319
330
|
issues.append("旧独立复核记录结论为空或未知")
|
|
320
331
|
if method == "独立":
|
|
321
332
|
reviewed = completed_independent(row)
|
|
322
|
-
if outcome == "failed":
|
|
333
|
+
if outcome == "failed" and not review_unavailable(row[3]):
|
|
323
334
|
unresolved = row[3]
|
|
324
335
|
elif row[1] == "复核基线复用":
|
|
325
336
|
referenced = [source for source in reusable_prior_phases if source in row[4]]
|
|
@@ -328,9 +339,7 @@ def legacy_review_state(plan_text, phase):
|
|
|
328
339
|
elif outcome == "passed" and qualified(row):
|
|
329
340
|
reviewed = True
|
|
330
341
|
elif row[1] == "修复自验":
|
|
331
|
-
if
|
|
332
|
-
issues.append("旧格式修复自验缺少已完成的独立复核基线")
|
|
333
|
-
elif outcome == "passed" and qualified(row):
|
|
342
|
+
if outcome == "passed" and qualified(row) and reviewed:
|
|
334
343
|
unresolved = None
|
|
335
344
|
if outcome == "passed" and not qualified(row):
|
|
336
345
|
issues.append("旧独立复核记录通过缺少有效日期、类型、证据或身份")
|
|
@@ -346,15 +355,19 @@ def legacy_review_state(plan_text, phase):
|
|
|
346
355
|
review.get(field) == current[-1][index]
|
|
347
356
|
for field, index in [("日期", 0), ("阶段", 2), ("结论", 3), ("证据", 4), ("复核者", 5)]
|
|
348
357
|
)
|
|
349
|
-
|
|
358
|
+
summary = key_value_table(fixed_section(top_level_section(plan_text, "当前阶段"), "阶段准入摘要"))
|
|
359
|
+
requested = independent_review_requested(summary)
|
|
360
|
+
awaiting_requested_review = requested and outcome == "passed" and not reviewed
|
|
361
|
+
result["passed"] = outcome == "passed" and latest_matches and not issues and not blockers and not awaiting_requested_review
|
|
350
362
|
if outcome == "pending" and not issues and not blockers:
|
|
351
|
-
|
|
352
|
-
|
|
363
|
+
result["pending_action"] = "independent_review" if requested else "verify"
|
|
364
|
+
elif awaiting_requested_review and not issues and not blockers:
|
|
365
|
+
result["pending_action"] = "independent_review"
|
|
353
366
|
return result
|
|
354
367
|
|
|
355
368
|
|
|
356
369
|
def risk_review_state(plan_text, phase):
|
|
357
|
-
"""
|
|
370
|
+
"""显式风险策略的共享事实判定;历史策略名不改变默认自验行为。"""
|
|
358
371
|
summary_section = fixed_section(top_level_section(plan_text, "当前阶段"), "阶段准入摘要")
|
|
359
372
|
policies = [row for row in markdown_table_rows(summary_section) if row[0] == "复核策略"]
|
|
360
373
|
result = {"enabled": bool(policies), "passed": False, "pending_action": None,
|
|
@@ -362,8 +375,8 @@ def risk_review_state(plan_text, phase):
|
|
|
362
375
|
if not policies:
|
|
363
376
|
return result
|
|
364
377
|
issues, blockers = result["issues"], result["blockers"]
|
|
365
|
-
if len(policies) != 1 or len(policies[0]) != 2 or policies[0][1] not in {"风险分流", "单次独立复核"}:
|
|
366
|
-
issues.append("复核策略必须唯一且为
|
|
378
|
+
if len(policies) != 1 or len(policies[0]) != 2 or policies[0][1] not in {"风险分流", "单次独立复核", "默认自验"}:
|
|
379
|
+
issues.append("复核策略必须唯一且为 默认自验/风险分流/单次独立复核;空值、未知值或重复声明不能准入")
|
|
367
380
|
risks = {"低风险", "高影响", "高风险"}
|
|
368
381
|
summary = key_value_table(summary_section)
|
|
369
382
|
if "最新独立准入复核" in summary:
|
|
@@ -464,7 +477,7 @@ def risk_review_state(plan_text, phase):
|
|
|
464
477
|
else:
|
|
465
478
|
state["reviewed_high"] = False
|
|
466
479
|
row_outcome = review_outcome(row[5])
|
|
467
|
-
if row_outcome == "failed":
|
|
480
|
+
if row_outcome == "failed" and not review_unavailable(row[5]):
|
|
468
481
|
state["unresolved"] = True
|
|
469
482
|
elif (row[1] == "修复自验" and state["reviewed_high"]
|
|
470
483
|
and review_outcome(row[5]) == "passed"
|
|
@@ -489,7 +502,7 @@ def risk_review_state(plan_text, phase):
|
|
|
489
502
|
reviewed_high = row[4] in {"高风险", "高影响"}
|
|
490
503
|
else:
|
|
491
504
|
reviewed = reviewed_high = False
|
|
492
|
-
if review_outcome(row[5]) == "failed":
|
|
505
|
+
if review_outcome(row[5]) == "failed" and not review_unavailable(row[5]):
|
|
493
506
|
unresolved_single = row[5]
|
|
494
507
|
elif review_outcome(row[5]) == "passed" and completed(row):
|
|
495
508
|
# 后续独立通过可形成新范围基线,但不能清除既有独立发现。
|
|
@@ -503,12 +516,8 @@ def risk_review_state(plan_text, phase):
|
|
|
503
516
|
and not any(is_placeholder(value) for value in row)
|
|
504
517
|
and valid_date(row[0]) is not None):
|
|
505
518
|
reviewed = reviewed_high = True
|
|
506
|
-
elif row[4] in {"高风险", "高影响"} and not reviewed_high:
|
|
507
|
-
issues.append("高风险/高影响自验缺少同范围独立复核基线或明确的跨阶段复用记录")
|
|
508
519
|
if row[1] == "修复自验":
|
|
509
|
-
if
|
|
510
|
-
issues.append("修复自验缺少已完成的独立复核基线")
|
|
511
|
-
elif (review_outcome(row[5]) == "passed" and row[4] in risks
|
|
520
|
+
if (reviewed and review_outcome(row[5]) == "passed" and row[4] in risks
|
|
512
521
|
and not any(is_placeholder(value) for value in row)
|
|
513
522
|
and valid_date(row[0]) is not None):
|
|
514
523
|
unresolved_single = None
|
|
@@ -560,7 +569,7 @@ def risk_review_state(plan_text, phase):
|
|
|
560
569
|
row_outcome = review_outcome(row[3])
|
|
561
570
|
if not qualified(row, legacy=True):
|
|
562
571
|
issues.append("旧独立复核记录缺少有效日期、类型、证据或身份,迁移不能掩盖")
|
|
563
|
-
if row_outcome == "failed":
|
|
572
|
+
if row_outcome == "failed" and not review_unavailable(row[3]):
|
|
564
573
|
unresolved = row[3]
|
|
565
574
|
elif row_outcome == "passed" and qualified(row, legacy=True):
|
|
566
575
|
pass
|
|
@@ -581,15 +590,19 @@ def risk_review_state(plan_text, phase):
|
|
|
581
590
|
elif not legacy_history or any(is_placeholder(legacy.get(field))
|
|
582
591
|
for field in ["日期", "阶段", "结论", "证据", "复核者"]) or valid_date(legacy.get("日期", "")) is None:
|
|
583
592
|
issues.append("旧最新独立准入复核缺少有效字段或当前阶段历史,不能降级放行")
|
|
584
|
-
|
|
593
|
+
requested = independent_review_requested(summary)
|
|
594
|
+
awaiting_requested_review = requested and outcome == "passed" and not reviewed
|
|
595
|
+
result["passed"] = outcome == "passed" and not issues and not blockers and not awaiting_requested_review
|
|
585
596
|
if outcome == "pending" and not issues and not blockers:
|
|
586
|
-
result["pending_action"] = "
|
|
597
|
+
result["pending_action"] = "independent_review" if requested else "verify"
|
|
598
|
+
elif awaiting_requested_review and not issues and not blockers:
|
|
599
|
+
result["pending_action"] = "independent_review"
|
|
587
600
|
return result
|
|
588
601
|
|
|
589
602
|
|
|
590
|
-
def check_phase_structure(plan_name, data, plan_text, strict, warnings, errors, review_state=None):
|
|
603
|
+
def check_phase_structure(plan_name, data, plan_text, strict, warnings, errors, review_state=None, selected=False):
|
|
591
604
|
"""检查待实施/实施中计划的阶段准入结构,不判断业务证据真实性。"""
|
|
592
|
-
if data["status"] not in ACTIVE:
|
|
605
|
+
if data["status"] not in ACTIVE and not selected:
|
|
593
606
|
return
|
|
594
607
|
|
|
595
608
|
current_phase = data["phase"]
|
|
@@ -748,7 +761,7 @@ def check_phase_structure(plan_name, data, plan_text, strict, warnings, errors,
|
|
|
748
761
|
warnings,
|
|
749
762
|
errors,
|
|
750
763
|
strict,
|
|
751
|
-
f"{plan_name}:
|
|
764
|
+
f"{plan_name}: 旧格式当前阶段记录未满足自验或已请求复核的准入",
|
|
752
765
|
)
|
|
753
766
|
|
|
754
767
|
|
|
@@ -1439,6 +1452,8 @@ def create_attestation(
|
|
|
1439
1452
|
attest_files=None,
|
|
1440
1453
|
):
|
|
1441
1454
|
binding = None
|
|
1455
|
+
if errors:
|
|
1456
|
+
return None
|
|
1442
1457
|
if attest_files is not None:
|
|
1443
1458
|
if errors:
|
|
1444
1459
|
return None
|
|
@@ -1554,7 +1569,7 @@ def attestation_issue(warnings, errors, strict, message):
|
|
|
1554
1569
|
|
|
1555
1570
|
|
|
1556
1571
|
def warn_attestation_drift(warnings, root, plans, errors=None, strict=False, reports=None,
|
|
1557
|
-
pending=None, enforce_binding_review=True):
|
|
1572
|
+
pending=None, enforce_binding_review=True, selected=None):
|
|
1558
1573
|
errors = errors if errors is not None else []
|
|
1559
1574
|
attestations_dir = root / "docs" / "attestations"
|
|
1560
1575
|
try:
|
|
@@ -1586,6 +1601,8 @@ def warn_attestation_drift(warnings, root, plans, errors=None, strict=False, rep
|
|
|
1586
1601
|
bound = "binding" in attestation
|
|
1587
1602
|
invalid_structure = False
|
|
1588
1603
|
plan_name = str(attestation.get("plan", "")).strip()
|
|
1604
|
+
if selected and plan_name in plans and plan_name not in selected:
|
|
1605
|
+
continue
|
|
1589
1606
|
if not plan_name or plan_name not in plans:
|
|
1590
1607
|
message = f"{path}: 快照引用了未登记计划:{plan_name or '<missing>'}"
|
|
1591
1608
|
if not bound:
|
|
@@ -1885,6 +1902,10 @@ def plan_index_issues(plan_map_text):
|
|
|
1885
1902
|
status = row[1].strip("` ")
|
|
1886
1903
|
if status not in VALID_STATUSES:
|
|
1887
1904
|
issues.append(f"{name}: 计划状态非法:{status}")
|
|
1905
|
+
try:
|
|
1906
|
+
parse_plan_date(row[3].strip("` "))
|
|
1907
|
+
except ValueError:
|
|
1908
|
+
issues.append(f"{name}: 最后更新日期不合法:{row[3]}")
|
|
1888
1909
|
return issues
|
|
1889
1910
|
|
|
1890
1911
|
|
|
@@ -1979,12 +2000,12 @@ def structural_ambiguities(plan_text, phase):
|
|
|
1979
2000
|
return issues
|
|
1980
2001
|
|
|
1981
2002
|
|
|
1982
|
-
def phase_gate(plan_map_text, plan_name, data, plan_text):
|
|
2003
|
+
def phase_gate(plan_map_text, plan_name, data, plan_text, selected=False):
|
|
1983
2004
|
"""共享只读派生;严重级别由 check/workset/hook 的入口契约决定。"""
|
|
1984
2005
|
issues = structural_ambiguities(plan_text, data["phase"])
|
|
1985
2006
|
warnings = []
|
|
1986
2007
|
review_state = risk_review_state(plan_text, data["phase"])
|
|
1987
|
-
check_phase_structure(plan_name, data, plan_text, True, warnings, issues, review_state)
|
|
2008
|
+
check_phase_structure(plan_name, data, plan_text, True, warnings, issues, review_state, selected=selected)
|
|
1988
2009
|
if review_state["enabled"]:
|
|
1989
2010
|
issues.extend(review_state["issues"] + review_state["blockers"])
|
|
1990
2011
|
if data["status"] in ACTIVE and is_placeholder(data["phase"]):
|
|
@@ -1995,17 +2016,28 @@ def phase_gate(plan_map_text, plan_name, data, plan_text):
|
|
|
1995
2016
|
indexed = mapped.get(plan_name, [])
|
|
1996
2017
|
summary = current_summary(plan_text)
|
|
1997
2018
|
value = summary.get("当前阻塞项", "").strip()
|
|
1998
|
-
declared = [value] if value and not is_placeholder(value) else []
|
|
2019
|
+
declared = [value] if value and not is_placeholder(value) and not re.match(r"^无(?:[;;,,。]|$)", value) else []
|
|
1999
2020
|
blockers = list(dict.fromkeys(details + indexed + declared))
|
|
2021
|
+
issue_codes = {}
|
|
2000
2022
|
if blockers:
|
|
2001
|
-
|
|
2023
|
+
message = "当前阶段存在未解决阻塞项:" + ";".join(blockers)
|
|
2024
|
+
issues.append(message)
|
|
2025
|
+
issue_codes[message] = "CURRENT_PHASE_BLOCKED"
|
|
2002
2026
|
if ((details or indexed) and value == "无") or ((details or declared) and not indexed):
|
|
2003
2027
|
issues.append("当前阻塞来源需同步:PLAN_MAP 索引、未决问题或阶段准入摘要缺失/冲突")
|
|
2004
2028
|
if review_state["enabled"]:
|
|
2005
2029
|
blockers.extend(review_state["blockers"])
|
|
2006
2030
|
else:
|
|
2007
2031
|
legacy_state = legacy_review_state(plan_text, data["phase"])
|
|
2008
|
-
|
|
2032
|
+
history = [r for r in review_history_rows(plan_text) if len(r) == 6 and r[2] == data["phase"]]
|
|
2033
|
+
completion_positions = [i for i, r in enumerate(history)
|
|
2034
|
+
if "完成" in r[1] and review_outcome(r[3]) == "passed"
|
|
2035
|
+
and valid_review_date(r[0]) is not None
|
|
2036
|
+
and not any(is_placeholder(v) for v in r)]
|
|
2037
|
+
completed_review = (data["status"] in COMPLETED and completion_positions
|
|
2038
|
+
and all(review_outcome(r[3]) == "passed" for r in history[completion_positions[-1]:]))
|
|
2039
|
+
if not completed_review:
|
|
2040
|
+
blockers.extend(legacy_state["blockers"])
|
|
2009
2041
|
review = key_value_table(fixed_section(plan_text, "最新独立准入复核"))
|
|
2010
2042
|
conclusion = review.get("结论", "").strip()
|
|
2011
2043
|
if review.get("阶段") == data["phase"] and conclusion.startswith(("未通过", "不通过", "失败", "不满足", "拒绝")):
|
|
@@ -2016,17 +2048,26 @@ def phase_gate(plan_map_text, plan_name, data, plan_text):
|
|
|
2016
2048
|
warnings.extend(recent_warnings)
|
|
2017
2049
|
issues = [message.removeprefix(f"{plan_name}: ") for message in issues]
|
|
2018
2050
|
return {"blockers": list(dict.fromkeys(blockers)), "issues": list(dict.fromkeys(issues)),
|
|
2019
|
-
"warnings": warnings, "review": review_state}
|
|
2051
|
+
"warnings": warnings, "review": review_state, "issue_codes": issue_codes}
|
|
2020
2052
|
|
|
2021
2053
|
|
|
2022
|
-
def check_phase_readiness(plan_map_text, plan_name, data, plan_text, strict, warnings, errors):
|
|
2054
|
+
def check_phase_readiness(plan_map_text, plan_name, data, plan_text, strict, warnings, errors, selected=False):
|
|
2055
|
+
if selected and data["status"] in INACTIVE:
|
|
2056
|
+
readiness_issue(warnings, errors, strict, f"{plan_name}: 已废弃/替代/合并的计划不能准入")
|
|
2057
|
+
return
|
|
2023
2058
|
if data["status"] not in WARNING_ACTIVE:
|
|
2024
2059
|
return
|
|
2025
|
-
gate = phase_gate(plan_map_text, plan_name, data, plan_text)
|
|
2060
|
+
gate = phase_gate(plan_map_text, plan_name, data, plan_text, selected=selected)
|
|
2026
2061
|
warnings.extend(f"{plan_name}: {message}" for message in gate["warnings"])
|
|
2027
2062
|
for message in gate["issues"]:
|
|
2063
|
+
for target in (errors, warnings):
|
|
2064
|
+
if isinstance(target, DiagnosticList):
|
|
2065
|
+
sources = target.metadata["sources"]
|
|
2066
|
+
target.context(plan_name, data["phase"], category="readiness",
|
|
2067
|
+
code=gate["issue_codes"].get(message, "PHASE_NOT_READY"),
|
|
2068
|
+
path=sources[0]["path"] if sources else data["path"], section="当前阶段")
|
|
2028
2069
|
# 缺失材料的设计计划仍合法;其显式冲突和阻塞保持可见而不强制准入。
|
|
2029
|
-
readiness_issue(warnings, errors, strict and data["status"] in ACTIVE, f"{plan_name}: {message}")
|
|
2070
|
+
readiness_issue(warnings, errors, strict and (data["status"] in ACTIVE or selected), f"{plan_name}: {message}")
|
|
2030
2071
|
|
|
2031
2072
|
|
|
2032
2073
|
def top_level_section(text, heading):
|
|
@@ -2330,10 +2371,244 @@ def recent_evidence(plan_text, phase=None):
|
|
|
2330
2371
|
return current_recent_evidence(plan_text, phase)[0]
|
|
2331
2372
|
|
|
2332
2373
|
|
|
2333
|
-
|
|
2374
|
+
class UsageError(ValueError):
|
|
2375
|
+
pass
|
|
2376
|
+
|
|
2377
|
+
|
|
2378
|
+
def validate_options(parser, argv, repeatable=()):
|
|
2379
|
+
"""Argparse handles values/types; reject ambiguous duplicate declarations."""
|
|
2380
|
+
seen = set()
|
|
2381
|
+
for token in argv:
|
|
2382
|
+
if token == "--":
|
|
2383
|
+
break
|
|
2384
|
+
option = token.split("=", 1)[0]
|
|
2385
|
+
action = parser._option_string_actions.get(option)
|
|
2386
|
+
if action is None or action.dest in repeatable:
|
|
2387
|
+
continue
|
|
2388
|
+
if action.dest in seen:
|
|
2389
|
+
parser.error(f"{option} 只能指定一次")
|
|
2390
|
+
seen.add(action.dest)
|
|
2391
|
+
|
|
2392
|
+
|
|
2393
|
+
def nonempty(value):
|
|
2394
|
+
if not value.strip():
|
|
2395
|
+
raise argparse.ArgumentTypeError("参数值不能为空")
|
|
2396
|
+
return value
|
|
2397
|
+
|
|
2398
|
+
|
|
2399
|
+
def scoped_table(text, title, keep):
|
|
2400
|
+
section = top_level_section(text, title)
|
|
2401
|
+
if section is None:
|
|
2402
|
+
return text
|
|
2403
|
+
rows = markdown_table_rows(section)
|
|
2404
|
+
selected = [row for i, row in enumerate(rows) if i == 0 or keep(row)]
|
|
2405
|
+
replacement = "\n\n" + "\n".join("| " + " | ".join(row) + " |" for row in selected) + "\n\n"
|
|
2406
|
+
start = re.search(rf"^##\s+{re.escape(title)}\s*$", mask_fenced_code(text), re.MULTILINE).end()
|
|
2407
|
+
return text[:start] + replacement + text[start + len(section):]
|
|
2408
|
+
|
|
2409
|
+
|
|
2410
|
+
def scoped_map(text, names):
|
|
2411
|
+
for title in ("阶段关系", "机器可检查共享写入约束"):
|
|
2412
|
+
text = scoped_table(text, title, lambda row: len(row) < 3 or bool({clean_relation_value(row[0]), clean_relation_value(row[2])} & names))
|
|
2413
|
+
return text
|
|
2414
|
+
|
|
2415
|
+
|
|
2416
|
+
def historical_phase_review(text, phase):
|
|
2417
|
+
"""复用历史校验,不使用后续阶段的最新结果替代来源阶段。"""
|
|
2418
|
+
for title, width in (("阶段复核记录", 8), ("独立复核记录", 6)):
|
|
2419
|
+
rows = markdown_table_rows(fixed_section(text, title))
|
|
2420
|
+
expected = (["日期", "类型", "阶段", "方式", "风险", "结论", "证据", "复核者"]
|
|
2421
|
+
if width == 8 else ["日期", "类型", "阶段", "结论", "证据", "复核者"])
|
|
2422
|
+
relevant = [r for r in rows[1:] if len(r) < 3 or r[2] == phase or not valid_phase_name(r[2])]
|
|
2423
|
+
if relevant and (rows[0] != expected or rows.count(expected) > 1
|
|
2424
|
+
or any(len(r) != width or len(r) < 3 or not valid_phase_name(r[2]) for r in relevant)):
|
|
2425
|
+
return {"passed": False, "issues": [f"来源阶段 {phase} 的 {title} 结构无效"], "blockers": []}
|
|
2426
|
+
explicit = [r for r in markdown_table_rows(fixed_section(text, "阶段复核记录")) if len(r) == 8 and r[2] == phase]
|
|
2427
|
+
legacy = [r for r in review_history_rows(text) if len(r) == 6 and r[2] == phase]
|
|
2428
|
+
if explicit:
|
|
2429
|
+
row = explicit[-1]
|
|
2430
|
+
latest = dict(zip(["日期", "阶段", "方式", "风险", "结论", "证据", "复核者"], [row[0], row[2], *row[3:]]))
|
|
2431
|
+
latest["风险依据"] = "来源阶段历史证据"
|
|
2432
|
+
synthetic = "## 当前阶段\n\n### 阶段准入摘要\n\n| 字段 | 内容 |\n|---|---|\n| 复核策略 | 默认自验 |\n| 最新阶段复核 | #最新阶段复核 |\n\n## 最新阶段复核\n\n| 字段 | 内容 |\n|---|---|\n"
|
|
2433
|
+
synthetic += "\n".join(f"| {key} | {value} |" for key, value in latest.items())
|
|
2434
|
+
synthetic += "\n\n## 阶段复核记录\n\n| 日期 | 类型 | 阶段 | 方式 | 风险 | 结论 | 证据 | 复核者 |\n|---|---|---|---|---|---|---|---|\n"
|
|
2435
|
+
synthetic += "\n".join("| " + " | ".join(r) + " |" for r in explicit)
|
|
2436
|
+
return risk_review_state(synthetic, phase)
|
|
2437
|
+
if legacy:
|
|
2438
|
+
row = legacy[-1]
|
|
2439
|
+
synthetic = "## 最新独立准入复核\n\n| 字段 | 内容 |\n|---|---|\n"
|
|
2440
|
+
synthetic += "\n".join(f"| {key} | {value} |" for key, value in zip(["日期", "阶段", "结论", "证据", "复核者"], [row[0], *row[2:]]))
|
|
2441
|
+
synthetic += "\n\n## 独立复核记录\n\n| 日期 | 类型 | 阶段 | 结论 | 证据 | 复核者 |\n|---|---|---|---|---|---|\n"
|
|
2442
|
+
synthetic += "\n".join("| " + " | ".join(r) + " |" for r in legacy)
|
|
2443
|
+
return legacy_review_state(synthetic, phase)
|
|
2444
|
+
return {"passed": False, "issues": [f"来源阶段 {phase} 缺少有效复核历史"], "blockers": []}
|
|
2445
|
+
|
|
2446
|
+
|
|
2447
|
+
def build_scope(root, requested, text, plans):
|
|
2448
|
+
if len(requested) != len(set(requested)):
|
|
2449
|
+
raise UsageError("--plan 不接受重复计划 ID")
|
|
2450
|
+
unknown = set(requested) - set(plans)
|
|
2451
|
+
if unknown:
|
|
2452
|
+
raise UsageError("未登记的 --plan:" + "、".join(sorted(unknown)))
|
|
2453
|
+
scope = {"requested_plans": requested, "focused_phases": [{"plan": n, "phase": plans[n]["phase"]} for n in requested],
|
|
2454
|
+
"evaluated_plans": list(requested), "dependencies": [], "evidence_links": [], "shared_writes": [], "global_diagnostics": []}
|
|
2455
|
+
relation_header, relations = relation_rows(text)
|
|
2456
|
+
shared_header, shared = shared_write_rows(text)
|
|
2457
|
+
relations = [[clean_relation_value(cell) for cell in row] for row in relations]
|
|
2458
|
+
shared = [[clean_relation_value(cell) for cell in row] for row in shared]
|
|
2459
|
+
if relation_header == [] or shared_header == []:
|
|
2460
|
+
scope["global_diagnostics"].append("相关关系表缺少固定表头,无法可信确认作用域")
|
|
2461
|
+
queue = [(name, plans[name]["phase"]) for name in requested]
|
|
2462
|
+
visited = set()
|
|
2463
|
+
edges = {}
|
|
2464
|
+
for name, phase in queue:
|
|
2465
|
+
if (name, phase) in visited:
|
|
2466
|
+
continue
|
|
2467
|
+
visited.add((name, phase))
|
|
2468
|
+
# 已完成前置由完成证据核验,不重新开启其已经关闭的历史依赖链。
|
|
2469
|
+
if plans[name]["status"] in COMPLETED:
|
|
2470
|
+
continue
|
|
2471
|
+
incoming = [r for r in relations if len(r) == 7 and clean_relation_value(r[2]) == name and clean_relation_value(r[3]) == phase and r[4] == "hard_gate"]
|
|
2472
|
+
predecessors = [(clean_relation_value(r[0]), clean_relation_value(r[1]), r[5], r[6]) for r in incoming]
|
|
2473
|
+
explicit = {row[0] for row in predecessors}
|
|
2474
|
+
for dep in extract_declared_dependencies(plans[name]["depends"]) if phase == plans[name]["phase"] else []:
|
|
2475
|
+
if dep not in explicit:
|
|
2476
|
+
predecessors.append((dep, plans.get(dep, {}).get("phase", ""), "计划索引依赖", "PLAN_MAP.md"))
|
|
2477
|
+
for dep, source_phase, unlock, evidence in predecessors:
|
|
2478
|
+
if dep not in plans:
|
|
2479
|
+
scope["global_diagnostics"].append(f"{name}: 必要前置不存在:{dep}")
|
|
2480
|
+
continue
|
|
2481
|
+
edges.setdefault(f"{name}@{phase}", []).append(f"{dep}@{source_phase}")
|
|
2482
|
+
record = {"plan": dep, "phase": source_phase, "required_by": name, "reason": unlock, "evidence": evidence}
|
|
2483
|
+
if record not in scope["dependencies"]:
|
|
2484
|
+
scope["dependencies"].append(record)
|
|
2485
|
+
queue.append((dep, source_phase))
|
|
2486
|
+
for cycle in detect_dependency_cycles(edges):
|
|
2487
|
+
scope["global_diagnostics"].append(f"相关阶段依赖存在环:{cycle}")
|
|
2488
|
+
names = {name for name, _ in visited}
|
|
2489
|
+
scope["evaluated_plans"] = sorted(names)
|
|
2490
|
+
filtered = scoped_map(text, names)
|
|
2491
|
+
for row in relations:
|
|
2492
|
+
if len(row) == 7 and {row[0], row[2]} & names and row[4] in {"evidence", "soft_context"}:
|
|
2493
|
+
scope["evidence_links"].append({"plan": row[0], "phase": row[1], "required_by": row[2], "relation": row[4], "evidence": row[6]})
|
|
2494
|
+
for row in shared:
|
|
2495
|
+
if len(row) == 9 and {row[0], row[2]} & names:
|
|
2496
|
+
scope["shared_writes"].append({"plans": [row[0], row[2]], "targets": row[5], "order": row[6], "reason": row[7], "evidence": row[8]})
|
|
2497
|
+
# 同伴仅检查范围声明,不投影其业务状态。
|
|
2498
|
+
affected = {}
|
|
2499
|
+
for name, data in plans.items():
|
|
2500
|
+
if name in names or data["status"] in WARNING_ACTIVE:
|
|
2501
|
+
try:
|
|
2502
|
+
affected[name] = extract_affected_targets(data["path"].read_text(encoding="utf-8"))
|
|
2503
|
+
except (OSError, UnicodeError):
|
|
2504
|
+
affected[name] = []
|
|
2505
|
+
for name in requested:
|
|
2506
|
+
for peer, targets in affected.items():
|
|
2507
|
+
if peer in requested:
|
|
2508
|
+
continue
|
|
2509
|
+
overlap = sorted({a for a in affected.get(name, []) for b in targets if a == b or a.startswith(b.rstrip("/") + "/") or b.startswith(a.rstrip("/") + "/")})
|
|
2510
|
+
if overlap:
|
|
2511
|
+
scope["shared_writes"].append({"plans": [name, peer], "targets": overlap, "reason": "影响范围重叠;需要单一写入或串行合并"})
|
|
2512
|
+
projection = {"index": [r for r in table_rows(text, "计划索引") if r and (link := extract_plan_link(r[0])) and Path(link).stem in names],
|
|
2513
|
+
"relations": [r for r in relations if len(r) < 3 or {r[0], r[2]} & names],
|
|
2514
|
+
"shared": scope["shared_writes"], "headers": [relation_header, shared_header],
|
|
2515
|
+
"scope_issues": scope["global_diagnostics"], "identity_issues": plan_index_issues(text),
|
|
2516
|
+
"blockers": {k: v for k, v in map_blockers(text)[0].items() if k in names}, "blocker_issues": map_blockers(text)[1]}
|
|
2517
|
+
contents = {}
|
|
2518
|
+
for name in sorted(names):
|
|
2519
|
+
try:
|
|
2520
|
+
contents[name] = sha256_file(plans[name]["path"])
|
|
2521
|
+
except OSError:
|
|
2522
|
+
contents[name] = None
|
|
2523
|
+
scope["facts_version"] = hashlib.sha256(json.dumps([projection, contents], ensure_ascii=False, sort_keys=True).encode()).hexdigest()
|
|
2524
|
+
return scope, filtered
|
|
2525
|
+
|
|
2526
|
+
|
|
2527
|
+
def dependency_diagnostics(root, scope, text, plans):
|
|
2528
|
+
errors = list(scope["global_diagnostics"])
|
|
2529
|
+
for dep in scope["dependencies"]:
|
|
2530
|
+
name, phase = dep["plan"], dep["phase"]
|
|
2531
|
+
data = plans[name]
|
|
2532
|
+
read_errors = []
|
|
2533
|
+
body = read_utf8(data["path"], read_errors)
|
|
2534
|
+
reasons = list(read_errors)
|
|
2535
|
+
if data["status"] in INACTIVE:
|
|
2536
|
+
reasons.append("必要前置已废弃/替代,须确认有效承接")
|
|
2537
|
+
elif phase != data["phase"]:
|
|
2538
|
+
review = historical_phase_review(body, phase)
|
|
2539
|
+
reasons.extend(review["issues"] + review["blockers"])
|
|
2540
|
+
if not review["passed"]:
|
|
2541
|
+
reasons.append(f"来源阶段 {phase} 未通过")
|
|
2542
|
+
elif data["status"] in COMPLETED:
|
|
2543
|
+
if not has_completion_evidence(body) or not has_coverage_evidence(body):
|
|
2544
|
+
reasons.append("已完成前置缺少完成/验证证据")
|
|
2545
|
+
reasons.extend(phase_gate(text, name, data, body)["blockers"])
|
|
2546
|
+
else:
|
|
2547
|
+
gate = phase_gate(text, name, data, body)
|
|
2548
|
+
reasons.extend(gate["issues"])
|
|
2549
|
+
review = gate["review"] if gate["review"]["enabled"] else legacy_review_state(body, phase)
|
|
2550
|
+
if not review["passed"]:
|
|
2551
|
+
reasons.append("必要前置当前阶段尚未通过")
|
|
2552
|
+
dep["phase_passed"] = not reasons
|
|
2553
|
+
dep["blockers"] = list(dict.fromkeys(reasons))
|
|
2554
|
+
errors.extend(f"{name}@{phase}: {reason}" for reason in dep["blockers"])
|
|
2555
|
+
return list(dict.fromkeys(errors))
|
|
2556
|
+
|
|
2557
|
+
|
|
2558
|
+
def scope_unchanged(root, scope):
|
|
2559
|
+
errors = []
|
|
2560
|
+
text, _, plans = load_plan_records(root, errors)
|
|
2561
|
+
if errors or plan_index_issues(text):
|
|
2562
|
+
return False
|
|
2563
|
+
try:
|
|
2564
|
+
fresh, _ = build_scope(root, scope["requested_plans"], text, plans)
|
|
2565
|
+
return fresh["facts_version"] == scope["facts_version"]
|
|
2566
|
+
except (UsageError, OSError, ValueError):
|
|
2567
|
+
return False
|
|
2568
|
+
|
|
2569
|
+
|
|
2570
|
+
def workset_payload(root, include_history=False, strict=False, requested=None):
|
|
2571
|
+
if not requested:
|
|
2572
|
+
return unscoped_workset_payload(root, include_history, strict)
|
|
2573
|
+
errors = []
|
|
2574
|
+
text, _, plans = load_plan_records(root, errors)
|
|
2575
|
+
errors.extend(plan_index_issues(text))
|
|
2576
|
+
empty = {"schema_version": 1, "source": "derived", "plans": [], "warnings": errors,
|
|
2577
|
+
"scope": {"requested_plans": requested, "evaluated_plans": [], "dependencies": []}}
|
|
2578
|
+
if errors:
|
|
2579
|
+
return empty, 1
|
|
2580
|
+
scope, selected_map = build_scope(root, requested, text, plans)
|
|
2581
|
+
output, status = unscoped_workset_payload(root, include_history, strict, focus=requested, supplied=(selected_map, plans))
|
|
2582
|
+
dep_errors = dependency_diagnostics(root, scope, selected_map, plans)
|
|
2583
|
+
output["warnings"].extend(dep_errors)
|
|
2584
|
+
output["scope"] = scope
|
|
2585
|
+
blocked = {d["required_by"] for d in scope["dependencies"] if not d["phase_passed"]}
|
|
2586
|
+
while True:
|
|
2587
|
+
expanded = blocked | {d["required_by"] for d in scope["dependencies"] if d["plan"] in blocked}
|
|
2588
|
+
if expanded == blocked:
|
|
2589
|
+
break
|
|
2590
|
+
blocked = expanded
|
|
2591
|
+
for item in output["plans"]:
|
|
2592
|
+
if item["plan"] in blocked or scope["global_diagnostics"]:
|
|
2593
|
+
item["readiness"] = "blocked"
|
|
2594
|
+
item["next_action"] = {"state": "known", "kind": "resolve_blocker", "reason": "必要前置尚未满足,见 scope.dependencies"}
|
|
2595
|
+
if not scope_unchanged(root, scope):
|
|
2596
|
+
output["warnings"].append("相关事实在读取期间发生变化;请重读聚焦工作集")
|
|
2597
|
+
for item in output["plans"]:
|
|
2598
|
+
item["readiness"] = "unknown"
|
|
2599
|
+
item["next_action"] = {"state": "unknown", "kind": "unknown", "reason": "相关事实已变更"}
|
|
2600
|
+
status = 1 if strict else status
|
|
2601
|
+
return output, 1 if scope["global_diagnostics"] or (strict and dep_errors) else status
|
|
2602
|
+
|
|
2603
|
+
|
|
2604
|
+
def unscoped_workset_payload(root, include_history=False, strict=False, focus=None, supplied=None):
|
|
2334
2605
|
errors = []
|
|
2335
2606
|
strict_failures = []
|
|
2336
|
-
|
|
2607
|
+
document_failures = []
|
|
2608
|
+
if supplied:
|
|
2609
|
+
plan_map_text, plans = supplied
|
|
2610
|
+
else:
|
|
2611
|
+
plan_map_text, _, plans = load_plan_records(root, errors)
|
|
2337
2612
|
output = {
|
|
2338
2613
|
"schema_version": 1,
|
|
2339
2614
|
"source": "derived",
|
|
@@ -2355,20 +2630,23 @@ def workset_payload(root, include_history=False, strict=False):
|
|
|
2355
2630
|
strict_failures.extend(relation_errors)
|
|
2356
2631
|
|
|
2357
2632
|
for name, data in plans.items():
|
|
2358
|
-
if
|
|
2633
|
+
if focus and name not in focus:
|
|
2634
|
+
continue
|
|
2635
|
+
if not focus and not include_history and data["status"] not in WARNING_ACTIVE:
|
|
2359
2636
|
continue
|
|
2360
2637
|
path = data["path"]
|
|
2361
2638
|
if not path.exists():
|
|
2362
2639
|
message = f"{name}: 计划文件不存在:{path}"
|
|
2363
2640
|
output["warnings"].append(message)
|
|
2364
2641
|
strict_failures.append(message)
|
|
2642
|
+
document_failures.append(message)
|
|
2365
2643
|
continue
|
|
2366
2644
|
plan_text = read_utf8(path, errors)
|
|
2367
2645
|
summary = current_summary(plan_text)
|
|
2368
|
-
gate = phase_gate(plan_map_text, name, data, plan_text)
|
|
2646
|
+
gate = phase_gate(plan_map_text, name, data, plan_text, selected=bool(focus) and strict and data["status"] in WARNING_ACTIVE)
|
|
2369
2647
|
blockers = gate["blockers"]
|
|
2370
2648
|
output["warnings"].extend(f"{name}: {message}" for message in gate["issues"] + gate["warnings"])
|
|
2371
|
-
if data["status"] in ACTIVE:
|
|
2649
|
+
if data["status"] in ACTIVE or focus:
|
|
2372
2650
|
strict_failures.extend(gate["issues"])
|
|
2373
2651
|
uncertain = gate["issues"] or blocker_index_issues or relation_errors
|
|
2374
2652
|
if blockers:
|
|
@@ -2406,23 +2684,34 @@ def workset_payload(root, include_history=False, strict=False):
|
|
|
2406
2684
|
"state": "known" if kind else "unknown",
|
|
2407
2685
|
"kind": kind or "unknown",
|
|
2408
2686
|
"reason": ("当前阶段复核通过,待同步 PLAN_MAP 准入状态" if kind == "sync" else
|
|
2409
|
-
"
|
|
2687
|
+
"当前阶段待自验" if kind == "verify" else
|
|
2410
2688
|
"当前阶段待独立复核" if kind == "independent_review" else
|
|
2411
2689
|
"当前阶段复核声明无法确定"),
|
|
2412
2690
|
}
|
|
2413
2691
|
elif not current_review_passes(plan_text, data["phase"]):
|
|
2414
2692
|
legacy_state = legacy_review_state(plan_text, data["phase"])
|
|
2415
|
-
kind = legacy_state["pending_action"] or "
|
|
2693
|
+
kind = legacy_state["pending_action"] or "verify"
|
|
2416
2694
|
readiness = "design"
|
|
2417
2695
|
action = {
|
|
2418
2696
|
"state": "known",
|
|
2419
2697
|
"kind": kind,
|
|
2420
2698
|
"reason": ("当前阶段待自验" if kind == "verify" else
|
|
2421
|
-
"
|
|
2699
|
+
"用户已明确要求独立复核"),
|
|
2422
2700
|
}
|
|
2423
2701
|
else:
|
|
2424
2702
|
readiness = "design"
|
|
2425
2703
|
action = {"state": "unknown", "kind": "unknown", "reason": "缺少结构化下一动作"}
|
|
2704
|
+
elif focus and data["status"] in COMPLETED:
|
|
2705
|
+
valid = has_completion_evidence(plan_text) and has_coverage_evidence(plan_text) and not blockers
|
|
2706
|
+
readiness = "unknown"
|
|
2707
|
+
action = {"state": "unknown", "kind": "unknown", "reason": "当前完成声明已检查;终态无下一实施动作" if valid else "完成声明缺少有效证据"}
|
|
2708
|
+
if not valid:
|
|
2709
|
+
strict_failures.append(f"{name}: 完成声明缺少有效证据")
|
|
2710
|
+
document_failures.append(f"{name}: 完成声明缺少有效证据")
|
|
2711
|
+
elif focus and data["status"] in INACTIVE:
|
|
2712
|
+
readiness = "unknown"
|
|
2713
|
+
action = {"state": "unknown", "kind": "unknown", "reason": "终态计划不能准入"}
|
|
2714
|
+
strict_failures.append(f"{name}: 终态计划不能准入")
|
|
2426
2715
|
else:
|
|
2427
2716
|
readiness = "unknown"
|
|
2428
2717
|
action = {"state": "unknown", "kind": "unknown", "reason": "计划状态不在工作集派生范围"}
|
|
@@ -2447,7 +2736,7 @@ def workset_payload(root, include_history=False, strict=False):
|
|
|
2447
2736
|
output["plans"] = []
|
|
2448
2737
|
output["warnings"].extend(index_issues)
|
|
2449
2738
|
strict_failures.extend(index_issues)
|
|
2450
|
-
return output, 1 if strict and strict_failures else 0
|
|
2739
|
+
return output, 1 if errors or index_issues or document_failures or (strict and strict_failures) else 0
|
|
2451
2740
|
|
|
2452
2741
|
|
|
2453
2742
|
def print_workset_text(payload, strict=False):
|
|
@@ -2506,26 +2795,91 @@ def limit_workset_evidence(root, payload, limit):
|
|
|
2506
2795
|
return result
|
|
2507
2796
|
|
|
2508
2797
|
|
|
2509
|
-
def run_workset(root, as_json=False, include_history=False, strict=False, evidence_limit=None):
|
|
2510
|
-
payload, status = workset_payload(root, include_history=include_history, strict=strict)
|
|
2798
|
+
def run_workset(root, as_json=False, include_history=False, strict=False, evidence_limit=None, requested=None):
|
|
2799
|
+
payload, status = workset_payload(root, include_history=include_history, strict=strict, requested=requested)
|
|
2511
2800
|
if as_json:
|
|
2512
2801
|
if evidence_limit is not None:
|
|
2513
2802
|
payload = limit_workset_evidence(root, payload, evidence_limit)
|
|
2803
|
+
scope = payload.get("scope")
|
|
2804
|
+
if scope and scope.get("facts_version") and not scope_unchanged(root, scope):
|
|
2805
|
+
payload["warnings"].append("证据窗口读取期间相关事实发生变化;请重读聚焦工作集")
|
|
2806
|
+
for item in payload["plans"]:
|
|
2807
|
+
item["readiness"] = "unknown"
|
|
2808
|
+
item["next_action"] = {"state": "unknown", "kind": "unknown", "reason": "相关事实已变更"}
|
|
2809
|
+
status = 1 if strict else status
|
|
2514
2810
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
2515
2811
|
else:
|
|
2516
2812
|
print_workset_text(payload, strict=strict)
|
|
2517
2813
|
return status
|
|
2518
2814
|
|
|
2519
2815
|
|
|
2816
|
+
class DiagnosticList(list):
|
|
2817
|
+
def __init__(self, records, severity):
|
|
2818
|
+
super().__init__()
|
|
2819
|
+
self.records = records
|
|
2820
|
+
self.severity = severity
|
|
2821
|
+
self.context()
|
|
2822
|
+
|
|
2823
|
+
def context(self, plan=None, phase=None, category="document", code=None, path=None, section=None):
|
|
2824
|
+
self.metadata = {"plan": plan, "phase": phase, "category": category,
|
|
2825
|
+
"code": code or ("DOCUMENT_ERROR" if category == "document" else "PHASE_NOT_READY"),
|
|
2826
|
+
"sources": [{"path": str(path), "section": section}] if path else []}
|
|
2827
|
+
|
|
2828
|
+
def append(self, value):
|
|
2829
|
+
super().append(value)
|
|
2830
|
+
self.records.append({**self.metadata, "severity": self.severity, "message": value})
|
|
2831
|
+
|
|
2832
|
+
def extend(self, values):
|
|
2833
|
+
for value in values:
|
|
2834
|
+
self.append(value)
|
|
2835
|
+
|
|
2836
|
+
|
|
2837
|
+
def finish_check(args, records, scope=None, reports=None, attested_path=None, not_applicable=False):
|
|
2838
|
+
unique = {}
|
|
2839
|
+
for item in records:
|
|
2840
|
+
key = (item["plan"], item["phase"], item["code"], item["message"])
|
|
2841
|
+
if key not in unique or item["severity"] == "error":
|
|
2842
|
+
unique[key] = item
|
|
2843
|
+
diagnostics = list(unique.values())
|
|
2844
|
+
counts = {level + "s": sum(item["severity"] == level for item in diagnostics) for level in ("error", "warning")}
|
|
2845
|
+
status = "failed" if counts["errors"] else "not_applicable" if not_applicable else "warning" if counts["warnings"] else "passed"
|
|
2846
|
+
code = 1 if counts["errors"] else 0
|
|
2847
|
+
result = {"schema_version": 1, "command": "check", "mode": "readiness" if args.strict_readiness else "consistency",
|
|
2848
|
+
"status": status, "exit_code": code, "scope": scope or {"requested_plans": [], "evaluated_plans": getattr(args, "evaluated_plans", []), "dependencies": []},
|
|
2849
|
+
"summary": counts, "diagnostics": diagnostics}
|
|
2850
|
+
if reports:
|
|
2851
|
+
result["attestations"] = reports
|
|
2852
|
+
if attested_path:
|
|
2853
|
+
result["created_attestation"] = str(attested_path)
|
|
2854
|
+
if args.json:
|
|
2855
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
2856
|
+
else:
|
|
2857
|
+
for item in diagnostics:
|
|
2858
|
+
print(f"{item['severity'].upper()}: {item['message']}", file=sys.stderr if item["severity"] == "error" else sys.stdout)
|
|
2859
|
+
for report in reports or []:
|
|
2860
|
+
print(f"ATTESTATION: {report}")
|
|
2861
|
+
if attested_path:
|
|
2862
|
+
print(f"已创建完成快照:{attested_path}")
|
|
2863
|
+
if not code:
|
|
2864
|
+
if not_applicable:
|
|
2865
|
+
print("未执行:当前目录未启用计划治理。")
|
|
2866
|
+
elif counts["warnings"]:
|
|
2867
|
+
print(f"{'准入' if args.strict_readiness else '文档'}检查完成,有 {counts['warnings']} 项告警。")
|
|
2868
|
+
else:
|
|
2869
|
+
print("准入检查通过。" if args.strict_readiness else "计划治理检查通过(文档一致性)。")
|
|
2870
|
+
return code
|
|
2871
|
+
|
|
2872
|
+
|
|
2520
2873
|
def parse_args(argv):
|
|
2521
|
-
parser = argparse.ArgumentParser(description="
|
|
2522
|
-
parser.add_argument("root", nargs="?", default=
|
|
2874
|
+
parser = argparse.ArgumentParser(description="检查计划治理文档的一致性;--strict-readiness 用于准入。", allow_abbrev=False)
|
|
2875
|
+
parser.add_argument("root", nargs="?", type=nonempty, default=None, help="仓库根目录,默认当前目录。")
|
|
2523
2876
|
parser.add_argument("--workset", action="store_true", help="输出只读当前工作集。")
|
|
2524
2877
|
parser.add_argument("--json", action="store_true", help="以 JSON 输出工作集。")
|
|
2525
2878
|
parser.add_argument("--include-history", action="store_true", help="工作集包含历史计划。")
|
|
2526
2879
|
parser.add_argument("--evidence-limit", type=int, metavar="N",
|
|
2527
2880
|
help="仅工作集 JSON:每个计划显示末尾 N 条记录(正整数),省略时返回全部。")
|
|
2528
|
-
parser.add_argument("--root", dest="mode_root", help="
|
|
2881
|
+
parser.add_argument("--root", dest="mode_root", type=nonempty, help="仓库根目录,与 positional root 二选一。")
|
|
2882
|
+
parser.add_argument("--plan", action="append", type=nonempty, default=[], help="可重复:聚焦已登记计划 ID,各会话独立传入。")
|
|
2529
2883
|
parser.add_argument("--drift", action="store_true", help="检查工作区变更是否被活跃计划影响范围覆盖。")
|
|
2530
2884
|
parser.add_argument("--pre-commit", action="store_true", help="检查 staged 变更是否被活跃计划影响范围覆盖。")
|
|
2531
2885
|
parser.add_argument(
|
|
@@ -2533,15 +2887,15 @@ def parse_args(argv):
|
|
|
2533
2887
|
action="store_true",
|
|
2534
2888
|
help="将待实施/实施中计划的阶段准入结构缺陷从 WARNING 提升为 ERROR。",
|
|
2535
2889
|
)
|
|
2536
|
-
parser.add_argument("--attest", metavar="PLAN", help="为已登记计划创建或覆盖完成快照。")
|
|
2537
|
-
parser.add_argument("--attest-file", action="append", metavar="PATH",
|
|
2890
|
+
parser.add_argument("--attest", type=nonempty, metavar="PLAN", help="为已登记计划创建或覆盖完成快照。")
|
|
2891
|
+
parser.add_argument("--attest-file", type=nonempty, action="append", metavar="PATH",
|
|
2538
2892
|
help="可重复:与 --attest/--attest-purpose 显式绑定被审查的普通文件。")
|
|
2539
2893
|
parser.add_argument(
|
|
2540
2894
|
"--attest-purpose",
|
|
2541
2895
|
choices=sorted(ATTESTATION_PURPOSES),
|
|
2542
2896
|
help="创建带 purpose/snapshot_id 关系的完成快照;不传时保持旧 <plan>.json 格式。",
|
|
2543
2897
|
)
|
|
2544
|
-
parser.add_argument("--supersedes", help="新 attestation 要替代的仓库内相对快照路径。")
|
|
2898
|
+
parser.add_argument("--supersedes", type=nonempty, help="新 attestation 要替代的仓库内相对快照路径。")
|
|
2545
2899
|
parser.add_argument(
|
|
2546
2900
|
"--review-status",
|
|
2547
2901
|
choices=sorted(ATTESTATION_REVIEW_STATUSES),
|
|
@@ -2557,7 +2911,30 @@ def parse_args(argv):
|
|
|
2557
2911
|
default=None,
|
|
2558
2912
|
help="检查活跃计划是否超过 N 天未更新;省略 N 时默认 10 天。",
|
|
2559
2913
|
)
|
|
2914
|
+
validate_options(parser, argv, {"plan", "attest_file"})
|
|
2560
2915
|
args = parser.parse_args(argv)
|
|
2916
|
+
if args.mode_root is not None and args.root is not None:
|
|
2917
|
+
parser.error("root 与 --root 不能同时使用")
|
|
2918
|
+
args.root = args.mode_root or args.root or "."
|
|
2919
|
+
if len(args.plan) != len(set(args.plan)):
|
|
2920
|
+
parser.error("--plan 不接受重复 ID")
|
|
2921
|
+
if args.workset:
|
|
2922
|
+
if any((args.drift, args.pre_commit, args.stale_days is not None, args.attest, args.attest_file is not None, args.attest_purpose, args.supersedes, args.check_attestations)):
|
|
2923
|
+
parser.error("workset 不接受 check 的检查/快照参数")
|
|
2924
|
+
elif args.include_history:
|
|
2925
|
+
parser.error("--include-history 仅适用于 workset")
|
|
2926
|
+
if args.attest and any((args.plan, args.drift, args.pre_commit, args.workset)):
|
|
2927
|
+
parser.error("--attest 不能与 workset/plan/drift/pre-commit 混用")
|
|
2928
|
+
if args.attest_file is not None and (not args.attest or not args.attest_purpose):
|
|
2929
|
+
parser.error("--attest-file 必须与 --attest/--attest-purpose 一起使用")
|
|
2930
|
+
if (args.attest_purpose or args.supersedes) and not args.attest:
|
|
2931
|
+
parser.error("--attest-purpose/--supersedes 必须与 --attest 一起使用")
|
|
2932
|
+
if any(token.split("=", 1)[0] == "--review-status" for token in argv) and not args.attest_purpose:
|
|
2933
|
+
parser.error("--review-status 必须与 --attest/--attest-purpose 一起使用")
|
|
2934
|
+
if args.supersedes and not args.attest_purpose:
|
|
2935
|
+
parser.error("--supersedes 必须与 --attest-purpose 一起使用")
|
|
2936
|
+
if args.stale_days is not None and args.stale_days < 0:
|
|
2937
|
+
parser.error("--stale-days 必须是非负整数")
|
|
2561
2938
|
if args.evidence_limit is not None:
|
|
2562
2939
|
if args.evidence_limit <= 0:
|
|
2563
2940
|
parser.error("--evidence-limit 必须为正整数")
|
|
@@ -2571,63 +2948,61 @@ def main(argv=None):
|
|
|
2571
2948
|
if args.attest_file is not None and (not args.attest or not args.attest_purpose or args.workset):
|
|
2572
2949
|
print("ERROR: --attest-file 必须与 --attest/--attest-purpose 一起使用,不能用于 workset")
|
|
2573
2950
|
return 1
|
|
2951
|
+
root = Path(args.root).expanduser().resolve()
|
|
2574
2952
|
if args.workset:
|
|
2953
|
+
if not root.is_dir():
|
|
2954
|
+
payload = {"schema_version": 1, "source": "derived", "plans": [], "warnings": [f"root 不存在或不是目录:{root}"]}
|
|
2955
|
+
print(json.dumps(payload, ensure_ascii=False) if args.json else payload["warnings"][0])
|
|
2956
|
+
return 1
|
|
2575
2957
|
return run_workset(
|
|
2576
|
-
|
|
2958
|
+
root,
|
|
2577
2959
|
as_json=args.json,
|
|
2578
2960
|
include_history=args.include_history,
|
|
2579
2961
|
strict=args.strict_readiness,
|
|
2580
2962
|
evidence_limit=args.evidence_limit,
|
|
2963
|
+
requested=args.plan,
|
|
2581
2964
|
)
|
|
2582
|
-
root = Path(args.root)
|
|
2583
2965
|
docs = root / "docs"
|
|
2584
2966
|
plan_map = docs / "PLAN_MAP.md"
|
|
2585
|
-
|
|
2586
|
-
|
|
2967
|
+
records = []
|
|
2968
|
+
errors = DiagnosticList(records, "error")
|
|
2969
|
+
warnings = DiagnosticList(records, "warning")
|
|
2970
|
+
scope = None
|
|
2971
|
+
if not root.is_dir():
|
|
2972
|
+
errors.context(category="runtime", code="INVALID_ROOT")
|
|
2973
|
+
errors.append(f"root 不存在或不是目录:{root}")
|
|
2974
|
+
return finish_check(args, records)
|
|
2587
2975
|
|
|
2588
2976
|
try:
|
|
2589
2977
|
bound_input = args.attest_file is not None or (args.check_attestations and has_attestation_binding(root))
|
|
2590
2978
|
except (ValueError, OSError) as exc:
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
return 1 if args.strict_readiness else 0
|
|
2979
|
+
attestation_issue(warnings, errors, args.strict_readiness, f"快照清单无法读取:{exc}")
|
|
2980
|
+
return finish_check(args, records)
|
|
2594
2981
|
unsafe_map = False
|
|
2595
2982
|
if bound_input:
|
|
2596
2983
|
try:
|
|
2597
2984
|
binding_file(root, "docs/PLAN_MAP.md")
|
|
2598
2985
|
except (ValueError, OSError) as exc:
|
|
2599
2986
|
if args.attest_file is not None:
|
|
2600
|
-
|
|
2601
|
-
return
|
|
2987
|
+
errors.append(f"binding 创建预检失败:{exc}")
|
|
2988
|
+
return finish_check(args, records)
|
|
2602
2989
|
unsafe_map = True
|
|
2603
2990
|
if unsafe_map:
|
|
2604
2991
|
reports = []
|
|
2605
2992
|
warn_attestation_drift(warnings, root, {}, errors=errors, strict=args.strict_readiness, reports=reports)
|
|
2606
|
-
|
|
2607
|
-
print(f"WARNING: {warning}")
|
|
2608
|
-
for report in reports:
|
|
2609
|
-
print(f"ATTESTATION: {report}")
|
|
2610
|
-
for error in errors:
|
|
2611
|
-
print(f"ERROR: {error}")
|
|
2612
|
-
return 1 if errors else 0
|
|
2993
|
+
return finish_check(args, records, reports=reports)
|
|
2613
2994
|
if not plan_map.exists():
|
|
2995
|
+
reports = []
|
|
2614
2996
|
if args.check_attestations:
|
|
2615
|
-
reports = []
|
|
2616
2997
|
warn_attestation_drift(warnings, root, {}, errors=errors, strict=args.strict_readiness, reports=reports)
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
print(f"WARNING: {warning}")
|
|
2620
|
-
for report in reports:
|
|
2621
|
-
print(f"ATTESTATION: {report}")
|
|
2622
|
-
for error in errors:
|
|
2623
|
-
print(f"ERROR: {error}")
|
|
2624
|
-
return 1 if errors else 0
|
|
2625
|
-
print("未找到 docs/PLAN_MAP.md;当前仓库尚未初始化计划治理。")
|
|
2626
|
-
return 0
|
|
2998
|
+
readiness_issue(warnings, errors, args.strict_readiness or bool(args.plan), "未找到 docs/PLAN_MAP.md;当前仓库尚未初始化计划治理")
|
|
2999
|
+
return finish_check(args, records, reports=reports, not_applicable=not errors)
|
|
2627
3000
|
|
|
2628
3001
|
text = read_utf8(plan_map, errors)
|
|
2629
3002
|
_, blocker_index_issues = map_blockers(text)
|
|
2630
|
-
for issue in plan_index_issues(text)
|
|
3003
|
+
for issue in plan_index_issues(text):
|
|
3004
|
+
fail(errors, issue)
|
|
3005
|
+
for issue in blocker_index_issues:
|
|
2631
3006
|
readiness_issue(warnings, errors, args.strict_readiness, issue)
|
|
2632
3007
|
plan_rows = table_rows(text, "计划索引")
|
|
2633
3008
|
if not plan_rows:
|
|
@@ -2661,7 +3036,7 @@ def main(argv=None):
|
|
|
2661
3036
|
attestation_issue(warnings, errors, args.attest_file is not None or args.strict_readiness,
|
|
2662
3037
|
f"{name}: binding 计划读取预检失败:{exc}")
|
|
2663
3038
|
unsafe_plans.add(name)
|
|
2664
|
-
if not path.exists():
|
|
3039
|
+
if not path.exists() and (not args.plan or name in args.plan):
|
|
2665
3040
|
fail(errors, f"docs/PLAN_MAP.md: 引用的计划文件不存在:{link}")
|
|
2666
3041
|
plans[name] = {
|
|
2667
3042
|
"path": path,
|
|
@@ -2673,6 +3048,12 @@ def main(argv=None):
|
|
|
2673
3048
|
else:
|
|
2674
3049
|
fail(errors, f"docs/PLAN_MAP.md: 计划行缺少 docs/plans 链接:{row[0]}")
|
|
2675
3050
|
|
|
3051
|
+
if args.plan and not errors:
|
|
3052
|
+
scope, text = build_scope(root, args.plan, text, plans)
|
|
3053
|
+
elif args.plan:
|
|
3054
|
+
return finish_check(args, records)
|
|
3055
|
+
|
|
3056
|
+
args.evaluated_plans = list(plans)
|
|
2676
3057
|
readable_plans = {name: data for name, data in plans.items() if name not in unsafe_plans}
|
|
2677
3058
|
relation_errors, relation_warnings = validate_relation_graph(text, readable_plans)
|
|
2678
3059
|
warnings.extend(relation_warnings)
|
|
@@ -2685,31 +3066,38 @@ def main(argv=None):
|
|
|
2685
3066
|
inactive = {name for name, data in plans.items() if data["status"] in INACTIVE}
|
|
2686
3067
|
for name, data in plans.items():
|
|
2687
3068
|
deps = extract_declared_dependencies(data["depends"])
|
|
3069
|
+
if not scope:
|
|
3070
|
+
for missing in set(deps) - set(plans):
|
|
3071
|
+
errors.append(f"{name}: 声明依赖的计划不存在:{missing}")
|
|
2688
3072
|
edges[name] = [dep for dep in deps if dep in plans]
|
|
2689
|
-
if data["status"] in IMPLEMENTING:
|
|
3073
|
+
if data["status"] in IMPLEMENTING and (not scope or name in scope["evaluated_plans"]):
|
|
2690
3074
|
for dep in edges[name]:
|
|
2691
3075
|
if dep in inactive:
|
|
2692
3076
|
fail(errors, f"{name}: 实施中计划依赖了非活跃计划 {dep}")
|
|
2693
3077
|
|
|
2694
3078
|
for cycle in detect_dependency_cycles(edges):
|
|
2695
|
-
|
|
3079
|
+
if not scope:
|
|
3080
|
+
fail(errors, f"计划依赖存在环:{cycle}")
|
|
2696
3081
|
|
|
2697
|
-
|
|
2698
|
-
|
|
3082
|
+
if not scope:
|
|
3083
|
+
for orphan in find_orphan_plans(docs, plans):
|
|
3084
|
+
warn(warnings, f"{orphan}: docs/plans 中存在未登记到 PLAN_MAP.md 的孤立计划")
|
|
2699
3085
|
|
|
2700
3086
|
active_plan_targets = {}
|
|
2701
3087
|
plan_texts = {}
|
|
2702
3088
|
for name, data in plans.items():
|
|
2703
|
-
if name in unsafe_plans:
|
|
3089
|
+
if name in unsafe_plans or (scope and name not in args.plan):
|
|
2704
3090
|
continue
|
|
3091
|
+
for target in (errors, warnings):
|
|
3092
|
+
target.context(name, data["phase"], path=data["path"].relative_to(root))
|
|
2705
3093
|
plan_text = read_utf8(data["path"], errors)
|
|
2706
3094
|
plan_texts[name] = plan_text
|
|
2707
3095
|
if data["status"] in COMPLETED and not has_completion_evidence(plan_text):
|
|
2708
3096
|
fail(errors, f"{data['path']}: 已完成计划缺少有效 Step 0 证据或验证方式")
|
|
2709
3097
|
if data["status"] in COMPLETED and not has_coverage_evidence(plan_text):
|
|
2710
3098
|
fail(errors, f"{data['path']}: 已完成计划缺少测试覆盖率证据")
|
|
2711
|
-
if data["status"] in
|
|
2712
|
-
fail(errors, f"{data['path']}:
|
|
3099
|
+
if data["status"] in COMPLETED and phase_gate(text, name, data, plan_text)["blockers"]:
|
|
3100
|
+
fail(errors, f"{data['path']}: 已完成计划仍有当前阶段阻塞/失败")
|
|
2713
3101
|
if data["status"] in WARNING_ACTIVE:
|
|
2714
3102
|
targets = extract_affected_targets(plan_text)
|
|
2715
3103
|
plan_relative = data["path"].relative_to(root).as_posix()
|
|
@@ -2726,6 +3114,8 @@ def main(argv=None):
|
|
|
2726
3114
|
)
|
|
2727
3115
|
active_plan_targets[name] = targets
|
|
2728
3116
|
|
|
3117
|
+
for target in (errors, warnings):
|
|
3118
|
+
target.context(name, data["phase"], category="readiness", path=data["path"].relative_to(root), section="当前阶段")
|
|
2729
3119
|
check_phase_readiness(
|
|
2730
3120
|
text,
|
|
2731
3121
|
name,
|
|
@@ -2734,15 +3124,19 @@ def main(argv=None):
|
|
|
2734
3124
|
args.strict_readiness,
|
|
2735
3125
|
warnings,
|
|
2736
3126
|
errors,
|
|
3127
|
+
selected=bool(scope),
|
|
2737
3128
|
)
|
|
2738
3129
|
|
|
3130
|
+
for target in (errors, warnings):
|
|
3131
|
+
target.context(category="advisory", code="SHARED_WRITE_RISK")
|
|
2739
3132
|
for target, plan_names in detect_overlapping_targets(active_plan_targets).items():
|
|
2740
3133
|
warn(warnings, f"{target}: 多个活跃计划声明相同影响目标:{', '.join(plan_names)}")
|
|
2741
3134
|
|
|
2742
3135
|
known_plans = set(plans)
|
|
2743
3136
|
for name, data in plans.items():
|
|
2744
|
-
if data["status"] not in WARNING_ACTIVE:
|
|
3137
|
+
if data["status"] not in WARNING_ACTIVE or (scope and name not in args.plan):
|
|
2745
3138
|
continue
|
|
3139
|
+
warnings.context(name, data["phase"], category="advisory", code="DEPENDENCY_REFERENCE", path=data["path"].relative_to(root))
|
|
2746
3140
|
declared = set(edges.get(name, []))
|
|
2747
3141
|
referenced = extract_plan_references(plan_texts.get(name, ""), known_plans, name)
|
|
2748
3142
|
for missing in sorted(referenced - declared):
|
|
@@ -2750,9 +3144,31 @@ def main(argv=None):
|
|
|
2750
3144
|
for unreferenced in sorted(declared - referenced):
|
|
2751
3145
|
warn(warnings, f"{name}: PLAN_MAP.md 声明依赖 {unreferenced},但计划正文未引用")
|
|
2752
3146
|
|
|
3147
|
+
for target in (errors, warnings):
|
|
3148
|
+
target.context(category="advisory", code="OPTIONAL_CHECK")
|
|
2753
3149
|
try:
|
|
2754
3150
|
drift_files = changed_files(root) if args.drift else set()
|
|
2755
3151
|
pre_commit_files = changed_files(root, staged=True) if args.pre_commit else set()
|
|
3152
|
+
if scope:
|
|
3153
|
+
peer_targets = {}
|
|
3154
|
+
for peer, data in plans.items():
|
|
3155
|
+
if peer not in args.plan and data["path"].is_file():
|
|
3156
|
+
body = data["path"].read_text(encoding="utf-8")
|
|
3157
|
+
peer_targets[peer] = extract_affected_targets(body) + [data["path"].relative_to(root).as_posix()]
|
|
3158
|
+
def relevant_changes(files, staged):
|
|
3159
|
+
selected = set()
|
|
3160
|
+
for file in files:
|
|
3161
|
+
own = any(target_matches_path(t, file) for targets in active_plan_targets.values() for t in targets)
|
|
3162
|
+
peers = any(target_matches_path(t, file) for targets in peer_targets.values() for t in targets)
|
|
3163
|
+
if file == "docs/PLAN_MAP.md":
|
|
3164
|
+
owners = plan_map_change_owners(root, staged, set(plans))
|
|
3165
|
+
if owners and all(owner and not set(owner) & set(args.plan) for owner in owners):
|
|
3166
|
+
continue
|
|
3167
|
+
if own or not peers:
|
|
3168
|
+
selected.add(file)
|
|
3169
|
+
return selected
|
|
3170
|
+
drift_files = relevant_changes(drift_files, False)
|
|
3171
|
+
pre_commit_files = relevant_changes(pre_commit_files, True)
|
|
2756
3172
|
completion_window_targets = completed_plan_drift_targets(
|
|
2757
3173
|
plans,
|
|
2758
3174
|
plan_texts,
|
|
@@ -2789,8 +3205,16 @@ def main(argv=None):
|
|
|
2789
3205
|
if args.stale_days < 0:
|
|
2790
3206
|
fail(errors, f"--stale-days 必须是非负整数:{args.stale_days}")
|
|
2791
3207
|
else:
|
|
2792
|
-
warn_stale_plans(warnings, plans, args.stale_days)
|
|
2793
|
-
|
|
3208
|
+
warn_stale_plans(warnings, {n: d for n, d in plans.items() if not scope or n in args.plan}, args.stale_days)
|
|
3209
|
+
|
|
3210
|
+
if scope:
|
|
3211
|
+
errors.context(category="document", code="SCOPE_STRUCTURE_INVALID")
|
|
3212
|
+
errors.extend(scope["global_diagnostics"])
|
|
3213
|
+
warnings.context(category="readiness", code="DEPENDENCY_BLOCKED")
|
|
3214
|
+
errors.context(category="readiness", code="DEPENDENCY_BLOCKED")
|
|
3215
|
+
for message in dependency_diagnostics(root, scope, text, plans):
|
|
3216
|
+
if message not in scope["global_diagnostics"]:
|
|
3217
|
+
readiness_issue(warnings, errors, args.strict_readiness, message)
|
|
2794
3218
|
if args.check_attestations:
|
|
2795
3219
|
attestation_reports = []
|
|
2796
3220
|
warn_attestation_drift(
|
|
@@ -2800,6 +3224,7 @@ def main(argv=None):
|
|
|
2800
3224
|
errors=errors,
|
|
2801
3225
|
strict=args.strict_readiness,
|
|
2802
3226
|
reports=attestation_reports,
|
|
3227
|
+
selected=scope["evaluated_plans"] if scope else None,
|
|
2803
3228
|
)
|
|
2804
3229
|
else:
|
|
2805
3230
|
attestation_reports = []
|
|
@@ -2808,6 +3233,16 @@ def main(argv=None):
|
|
|
2808
3233
|
if (args.attest_purpose or args.supersedes) and not args.attest:
|
|
2809
3234
|
fail(errors, "--attest-purpose/--supersedes 必须与 --attest 一起使用")
|
|
2810
3235
|
if args.attest:
|
|
3236
|
+
target = plans.get(args.attest)
|
|
3237
|
+
if target and not errors and args.attest_purpose != "compliance":
|
|
3238
|
+
body = read_utf8(target["path"], errors)
|
|
3239
|
+
if target["status"] in INACTIVE:
|
|
3240
|
+
errors.append("终态目标不能创建完成/发布门快照")
|
|
3241
|
+
elif target["status"] not in COMPLETED:
|
|
3242
|
+
gate = phase_gate(text, args.attest, target, body, selected=True)
|
|
3243
|
+
errors.extend(gate["issues"])
|
|
3244
|
+
elif phase_gate(text, args.attest, target, body)["blockers"]:
|
|
3245
|
+
fail(errors, "完成快照目标仍有未解决阻塞")
|
|
2811
3246
|
attested_path = create_attestation(
|
|
2812
3247
|
root,
|
|
2813
3248
|
plan_map,
|
|
@@ -2820,23 +3255,19 @@ def main(argv=None):
|
|
|
2820
3255
|
attest_files=args.attest_file,
|
|
2821
3256
|
)
|
|
2822
3257
|
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
if errors:
|
|
2830
|
-
for error in errors:
|
|
2831
|
-
print(f"ERROR: {error}")
|
|
2832
|
-
return 1
|
|
2833
|
-
|
|
2834
|
-
if attested_path is not None:
|
|
2835
|
-
print(f"已创建完成快照:{attested_path}")
|
|
2836
|
-
|
|
2837
|
-
print("计划治理检查通过。")
|
|
2838
|
-
return 0
|
|
3258
|
+
if scope and not scope_unchanged(root, scope):
|
|
3259
|
+
errors.context(category="readiness", code="FACTS_CHANGED")
|
|
3260
|
+
warnings.context(category="readiness", code="FACTS_CHANGED")
|
|
3261
|
+
readiness_issue(warnings, errors, args.strict_readiness, "相关事实在读取期间发生变化;请重新检查")
|
|
3262
|
+
return finish_check(args, records, scope=scope, reports=attestation_reports, attested_path=attested_path)
|
|
2839
3263
|
|
|
2840
3264
|
|
|
2841
3265
|
if __name__ == "__main__":
|
|
2842
|
-
|
|
3266
|
+
try:
|
|
3267
|
+
raise SystemExit(main())
|
|
3268
|
+
except UsageError as exc:
|
|
3269
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
3270
|
+
raise SystemExit(2)
|
|
3271
|
+
except (OSError, ValueError) as exc:
|
|
3272
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
3273
|
+
raise SystemExit(1)
|