DCL7 精简文档并统一中文口径
这个提交包含在:
+72
-142
@@ -1,176 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate docs/feature-matrix.md against execution-contract CSV sources.
|
||||
"""Validate the lightweight DCL7 document status baseline.
|
||||
|
||||
Checks:
|
||||
- feature-matrix contains exactly one row per ID in CSV fact sources
|
||||
- every ID from CSVs appears in matrix
|
||||
- matrix does not contain unknown IDs
|
||||
- every matrix row status equals DONE
|
||||
- every fact-source CSV row is fully DONE on Dev/Review1/Regression axes
|
||||
DCL7 removed the old full feature matrix from the current docs tree. Historical
|
||||
details remain recoverable from git history and issue CSVs. This check now
|
||||
guards the current lightweight status docs and the latest issue CSV contract
|
||||
shape instead of requiring a large duplicated feature table.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
MATRIX_PATH = ROOT / "docs" / "feature-matrix.md"
|
||||
CSV_FACT_SOURCES = [
|
||||
ROOT / "issues" / "2026-02-09_13-39-02-p5-blueprint-v3.csv",
|
||||
ROOT / "issues" / "2026-02-09_23-30-00-p6-p8-remaining-work-master.csv",
|
||||
ROOT / "issues" / "2026-02-10_09-42-31-p9-maintenance-performance-matrix.csv",
|
||||
ROOT / "issues" / "2026-02-10_10-35-30-p10-production-hardening.csv",
|
||||
ROOT / "issues" / "2026-02-11-p11-p12-governance-mobile.csv",
|
||||
ROOT / "issues" / "2026-02-12_01-09-41-p13-l5-governance.csv",
|
||||
ROOT / "issues" / "2026-02-13_00-11-56-p14-l5-operationalization.csv",
|
||||
ROOT / "issues" / "2026-03-06_14-30-00-p15a-l5-permission-decouple.csv",
|
||||
ROOT / "issues" / "2026-03-06_14-30-00-p15b-l5-leave-v1.csv",
|
||||
ROOT / "issues" / "2026-03-07_13-47-05-p16a-l5-master-data-core.csv",
|
||||
ROOT / "issues" / "2026-03-07_16-22-07-p16b-l5-holiday-calendar.csv",
|
||||
ROOT / "issues" / "2026-03-07_17-14-39-p16c-l5-suppliers.csv",
|
||||
ROOT / "issues" / "2026-03-07_18-15-03-r9-g2-p19-roadmap-refresh.csv",
|
||||
ROOT / "issues" / "2026-03-07_18-16-00-g2-l5-cross-domain-bridge.csv",
|
||||
ROOT / "issues" / "2026-03-07_19-33-09-p17a-l5-procurement-core.csv",
|
||||
ROOT / "issues" / "2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv",
|
||||
ROOT / "issues" / "2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv",
|
||||
ROOT / "issues" / "2026-03-07_22-34-03-p18b-l5-asset-movements-inventory.csv",
|
||||
ROOT / "issues" / "2026-03-07_23-22-00-p19-l4-integration-readiness.csv",
|
||||
ROOT / "issues" / "2026-03-08_12-18-10-p19-im-1-l5-collaboration-adapter.csv",
|
||||
ROOT / "issues" / "2026-03-08_14-10-53-p19-erp-1-l5-masterdata-journal-bridge.csv",
|
||||
ROOT / "issues" / "2026-03-08_22-46-51-p19-erp-2-l5-procurement-commitment-archive.csv",
|
||||
ROOT / "issues" / "2026-03-09_00-10-00-p19-erp-3-l5-asset-ledger-summary.csv",
|
||||
ROOT / "issues" / "2026-03-09_10-15-00-r10-post-erp3-roadmap-refresh.csv",
|
||||
ROOT / "issues" / "2026-03-09_10-25-00-p19-mcp-1-l4-readonly-tools-baseline.csv",
|
||||
ROOT / "issues" / "2026-03-10_12-54-30-p19-ai-1-l5-suggestion-workbench-refresh.csv",
|
||||
ROOT / "issues" / "2026-03-10_17-32-21-p20-travel-1-l5-request-intake-refresh.csv",
|
||||
ROOT / "issues" / "2026-03-10_21-05-59-p20-travel-2-l5-corporate-payments-refresh.csv",
|
||||
ROOT / "issues" / "2026-03-11_09-57-28-p20-travel-3-l5-reconciliation-erp-bridge-refresh.csv",
|
||||
ROOT / "issues" / "2026-03-11_17-50-39-p21-pwa-1-l5-asset-scan-refresh.csv",
|
||||
ROOT / "issues" / "2026-03-12_04-32-13-p21-pwa-2-l5-offline-deeplink-refresh.csv",
|
||||
ROOT / "issues" / "2026-03-12_16-58-44-p22-prod-1-l5-production-hardening.csv",
|
||||
ROOT / "issues" / "2026-03-13_10-30-00-r11-contract-reconciliation-and-spec-refresh.csv",
|
||||
]
|
||||
MATRIX_PATH = ROOT / "docs" / "历史功能状态矩阵.md"
|
||||
STATE_MATRIX_PATH = ROOT / "docs" / "文档事实源状态矩阵.md"
|
||||
INDEX_PATH = ROOT / "docs" / "文档索引.md"
|
||||
ISSUES_DIR = ROOT / "issues"
|
||||
|
||||
ID_PATTERN = re.compile(r"^[A-Z][0-9][0-9A-Za-z-]*$")
|
||||
STATUS_INDEX = 6
|
||||
REQUIRED_COLUMNS = {
|
||||
"ID",
|
||||
"Title",
|
||||
"Description",
|
||||
"Acceptance",
|
||||
"Test_Method",
|
||||
"Tools",
|
||||
"Dev_Status",
|
||||
"Review1_Status",
|
||||
"Regression_Status",
|
||||
"Files",
|
||||
"Dependencies",
|
||||
"Notes",
|
||||
}
|
||||
|
||||
|
||||
def load_csv_rows(path: Path) -> list[dict[str, str]]:
|
||||
with path.open(encoding="utf-8", newline="") as fp:
|
||||
return list(csv.DictReader(fp))
|
||||
def read_text(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def collect_expected_ids_and_source_status(paths: list[Path]) -> tuple[list[str], list[str]]:
|
||||
ids: list[str] = []
|
||||
source_errors: list[str] = []
|
||||
def validate_latest_issue_csv() -> list[str]:
|
||||
errors: list[str] = []
|
||||
csv_files = sorted(ISSUES_DIR.glob("*.csv"))
|
||||
if not csv_files:
|
||||
return ["No issue CSV files found"]
|
||||
|
||||
latest = csv_files[-1]
|
||||
with latest.open(encoding="utf-8", newline="") as fp:
|
||||
reader = csv.DictReader(fp)
|
||||
columns = set(reader.fieldnames or [])
|
||||
missing = sorted(REQUIRED_COLUMNS - columns)
|
||||
if missing:
|
||||
errors.append(f"{latest.name} missing columns: {missing}")
|
||||
|
||||
rows = list(reader)
|
||||
if not rows:
|
||||
errors.append(f"{latest.name} has no issue rows")
|
||||
|
||||
for path in paths:
|
||||
rows = load_csv_rows(path)
|
||||
for row in rows:
|
||||
row_id = (row.get("ID") or "").strip()
|
||||
if not row_id:
|
||||
continue
|
||||
ids.append(row_id)
|
||||
for key in ("Dev_Status", "Review1_Status", "Regression_Status"):
|
||||
value = (row.get(key) or "").strip()
|
||||
if value not in {"TODO", "DOING", "DONE"}:
|
||||
errors.append(f"{latest.name}:{row.get('ID')} invalid {key}={value!r}")
|
||||
|
||||
dev_status = (row.get("Dev_Status") or "").strip().upper()
|
||||
review_status = (row.get("Review1_Status") or "").strip().upper()
|
||||
regression_status = (row.get("Regression_Status") or "").strip().upper()
|
||||
if not (dev_status == review_status == regression_status == "DONE"):
|
||||
source_errors.append(
|
||||
f"{path.name}:{row_id} source status not all DONE "
|
||||
f"(Dev={dev_status}, Review1={review_status}, Regression={regression_status})"
|
||||
)
|
||||
|
||||
return ids, source_errors
|
||||
|
||||
|
||||
def load_matrix_rows(path: Path) -> list[list[str]]:
|
||||
rows: list[list[str]] = []
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped.startswith("|"):
|
||||
continue
|
||||
if stripped.startswith("| ---"):
|
||||
continue
|
||||
|
||||
parts = [part.strip() for part in stripped.strip("|").split("|")]
|
||||
if not parts:
|
||||
continue
|
||||
|
||||
row_id = parts[0]
|
||||
if not ID_PATTERN.match(row_id):
|
||||
continue
|
||||
|
||||
rows.append(parts)
|
||||
|
||||
return rows
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors: list[str] = []
|
||||
|
||||
if not MATRIX_PATH.exists():
|
||||
errors.append(f"Missing matrix file: {MATRIX_PATH}")
|
||||
for path in (MATRIX_PATH, STATE_MATRIX_PATH, INDEX_PATH):
|
||||
if not path.exists():
|
||||
errors.append(f"Missing required doc: {path.relative_to(ROOT)}")
|
||||
|
||||
for source in CSV_FACT_SOURCES:
|
||||
if not source.exists():
|
||||
errors.append(f"Missing csv file: {source}")
|
||||
if not errors:
|
||||
matrix = read_text(MATRIX_PATH)
|
||||
state = read_text(STATE_MATRIX_PATH)
|
||||
index = read_text(INDEX_PATH)
|
||||
|
||||
required_matrix_markers = [
|
||||
"历史功能状态矩阵",
|
||||
"git show 5da05eb:docs/feature-matrix.md",
|
||||
"MIF7",
|
||||
"ME6-3",
|
||||
]
|
||||
for marker in required_matrix_markers:
|
||||
if marker not in matrix:
|
||||
errors.append(f"{MATRIX_PATH.relative_to(ROOT)} missing marker: {marker}")
|
||||
|
||||
for marker in ("中文优先", "已从当前树移除", "MIF7", "ME6-3"):
|
||||
if marker not in state and marker not in index:
|
||||
errors.append(f"status/index docs missing marker: {marker}")
|
||||
|
||||
errors.extend(validate_latest_issue_csv())
|
||||
|
||||
if errors:
|
||||
for err in errors:
|
||||
print(f"[ERR] {err}")
|
||||
return 1
|
||||
|
||||
expected_ids, source_status_errors = collect_expected_ids_and_source_status(CSV_FACT_SOURCES)
|
||||
matrix_rows = load_matrix_rows(MATRIX_PATH)
|
||||
|
||||
matrix_ids = [row[0] for row in matrix_rows]
|
||||
expected_id_set = set(expected_ids)
|
||||
matrix_id_set = set(matrix_ids)
|
||||
|
||||
missing_in_matrix = sorted(expected_id_set - matrix_id_set)
|
||||
extra_in_matrix = sorted(matrix_id_set - expected_id_set)
|
||||
|
||||
if missing_in_matrix:
|
||||
errors.append(f"IDs missing in matrix: {missing_in_matrix}")
|
||||
if extra_in_matrix:
|
||||
errors.append(f"Extra IDs in matrix: {extra_in_matrix}")
|
||||
|
||||
duplicate_matrix_ids = sorted({item for item in matrix_ids if matrix_ids.count(item) > 1})
|
||||
if duplicate_matrix_ids:
|
||||
errors.append(f"Duplicate IDs in matrix: {duplicate_matrix_ids}")
|
||||
|
||||
if len(matrix_ids) != len(expected_ids):
|
||||
errors.append(f"Row count mismatch: matrix={len(matrix_ids)} expected={len(expected_ids)}")
|
||||
|
||||
duplicate_expected_ids = sorted({item for item in expected_ids if expected_ids.count(item) > 1})
|
||||
if duplicate_expected_ids:
|
||||
errors.append(f"Duplicate IDs in fact-source CSVs: {duplicate_expected_ids}")
|
||||
|
||||
for row in matrix_rows:
|
||||
if len(row) <= STATUS_INDEX:
|
||||
errors.append(f"Malformed matrix row (too few columns): {row}")
|
||||
continue
|
||||
status = row[STATUS_INDEX].upper()
|
||||
if status != "DONE":
|
||||
errors.append(f"Non-DONE status in matrix for {row[0]}: {row[STATUS_INDEX]}")
|
||||
|
||||
errors.extend(source_status_errors)
|
||||
|
||||
if errors:
|
||||
print("[ERR] feature-matrix validation failed")
|
||||
print("[ERR] lightweight document status validation failed")
|
||||
for err in errors:
|
||||
print(f" - {err}")
|
||||
return 1
|
||||
|
||||
source_names = ", ".join(path.name for path in CSV_FACT_SOURCES)
|
||||
print(
|
||||
f"[OK] feature-matrix matches fact sources ({source_names}) "
|
||||
f"(IDs/status/count)"
|
||||
)
|
||||
print("[OK] lightweight document status baseline is valid")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户