Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 63 additions & 16 deletions scripts/build-active-wp.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,32 +53,74 @@ def norm_status(token: str) -> str:

# Строка-РП: `| 312 | P2 | **Название** | 🔄 | repo | 8h |`
# Done-вариант: `| ~~306~~ | ~~P3~~ | ~~Название~~ | ✅ | ~~repo~~ | ~~4h~~ |`
ROW_RE = re.compile(r"^\|\s*(?:~~)?(?:\*\*)?(\d{1,4})(?:\*\*)?(?:~~)?\s*\|")
ROW_RE = re.compile(
r"^\|\s*(?:~~)?(?:\*\*)?(?:WP-)?(\d{1,4})(?:(?:[-.][A-Za-z0-9]+)+)?(?:\*\*)?(?:~~)?\s*\|"
)

# Имя файла WP в inbox/archive: WP-NNN-... .md или WP-NNN.md или папка WP-NNN/
WP_NAME_RE = re.compile(r"^WP-(\d{1,4})(?:[-.].*|/)?$")

COLUMN_SYNONYMS = {
"Приоритет": "P",
"Статус": "Ст",
"Репозитории": "Репо",
"Репозиторий": "Репо",
}


def find_registry_columns(lines: list[str]) -> dict[str, int] | None:
"""Возвращает позиции колонок основной таблицы реестра по её заголовку.

Старые реестры могли хранить только ``# | Название | Статус`` или
дополняться новыми колонками в конце. Поэтому порядок колонок не является
частью контракта: обязательны только номер, название и статус.
"""
for index, line in enumerate(lines[:-1]):
headers = [cell.strip() for cell in line.strip().strip("|").split("|")]
if "#" not in headers or not lines[index + 1].strip().startswith("|---"):
continue
columns: dict[str, int] = {}
for column_index, name in enumerate(headers):
columns.setdefault(COLUMN_SYNONYMS.get(name, name), column_index)
if {"#", "Название", "Ст"}.issubset(columns):
return columns
return None


def parse_registry(text: str) -> tuple[list[dict], list[str]]:
"""Разбор реестра. Строка с номером РП никогда не сбрасывается молча:
непарсибельные попадают в rows (для orphan-детекции) + в problems (PARSE-WARN)."""
rows: list[dict] = []
problems: list[str] = []
for lineno, line in enumerate(text.splitlines(), 1):
lines = text.splitlines()
columns = find_registry_columns(lines)
if columns is None:
problems.append(
"Заголовок основной таблицы реестра не распознан; используется "
"устаревшая позиционная схема."
)

def cell(cells: list[str], name: str, fallback: int | None = None) -> str:
index = columns.get(name) if columns is not None else fallback
if index is None or index >= len(cells):
return "—"
return cells[index].strip()

for lineno, line in enumerate(lines, 1):
m = ROW_RE.match(line)
if not m:
continue
wp = int(m.group(1))
cols = [c.strip() for c in line.strip("|").split("|")]
if len(cols) < 6:
if columns is None and len(cols) < 6:
problems.append(
f"WP-{wp} (строка {lineno}): колонок < 6 — строка учтена в реестре, "
f"но не попадает в active-wp.md."
)
cols = cols + [""] * (6 - len(cols))
# Очистка от ~~ и пробелов; берём только первый токен, чтобы
# принять варианты вида "🔄 Ф4" (статус + пометка фазы).
status_raw = cols[3].replace("~~", "").strip()
status_raw = cell(cols, "Ст", 3).replace("~~", "").strip()
token = status_raw.split()[0] if status_raw else ""
status = norm_status(token)
if status not in ALL_STATUSES:
Expand All @@ -88,24 +130,29 @@ def parse_registry(text: str) -> tuple[list[dict], list[str]]:
)
rows.append({
"wp": wp,
"project": cols[1].replace("~~", "").strip(),
"name": cols[2].strip(),
"id_display": cell(cols, "#", 0),
"project": cell(cols, "P", 1).replace("~~", "").strip(),
"name": cell(cols, "Название", 2),
"status": status,
"status_display": token,
"repo": cols[4].strip(),
"budget": cols[5].strip(),
"repo": cell(cols, "Репо", 4),
"budget": cell(cols, "Бюджет", 5),
"raw": line,
})
return rows, problems


def clean_status_in_row(raw: str, status: str) -> str:
"""Заменяет содержимое колонки «Ст» на очищенный статус и обрезает лишние колонки."""
parts = raw.split("|")
if len(parts) >= 6:
parts[4] = f" {status} "
parts = parts[:7]
return "|".join(parts) + "|"
def render_registry_row(row: dict) -> str:
"""Рендерит строку производного списка в его каноническом порядке колонок."""
cells = [
row["id_display"],
row["project"],
row["name"],
row["status_display"],
row["repo"],
row["budget"],
]
return "| " + " | ".join(cells) + " |"


def render(rows: list[dict]) -> str:
Expand All @@ -126,7 +173,7 @@ def table(items: list[dict]) -> str:
out = ["| # | P | Название | Ст | Репо | Бюджет |",
"|---:|---|------------------|:--:|------------------|------:|"]
for r in items:
out.append(clean_status_in_row(r["raw"], r["status_display"]))
out.append(render_registry_row(r))
return "\n".join(out) + "\n"

lines = [
Expand Down
5 changes: 4 additions & 1 deletion scripts/day-open-scaffold.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ IWE="$(iwe_resolve_root)"
IWE_ROOT="$IWE"
export IWE_ROOT IWE
DATE="${1:-$(date +%Y-%m-%d)}"
CONFIG="$IWE/${IWE_GOVERNANCE_REPO:-DS-strategy}/exocortex/day-rhythm-config.yaml"
CONFIG="$IWE/${IWE_GOVERNANCE_REPO:-DS-strategy}/day-rhythm-config.yaml"
if [ ! -f "$CONFIG" ]; then
CONFIG="$IWE/${IWE_GOVERNANCE_REPO:-DS-strategy}/exocortex/day-rhythm-config.yaml"
fi
PARAMS_FILE="$IWE/params.yaml"
MULTIPLIER_ENABLED="true"
if [ -f "$PARAMS_FILE" ] && grep -qE '^multiplier_enabled:[[:space:]]*false([[:space:]]*(#.*)?)?$' "$PARAMS_FILE"; then
Expand Down
76 changes: 76 additions & 0 deletions scripts/tests/test_build_active_wp_registry_columns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Регрессия: build-active-wp читает старый реестр по именам колонок."""

import importlib.util
from pathlib import Path


BUILD_ACTIVE_WP = Path(__file__).parent.parent / "build-active-wp.py"
SPEC = importlib.util.spec_from_file_location("build_active_wp", BUILD_ACTIVE_WP)
assert SPEC and SPEC.loader
BUILD = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(BUILD)


def test_reordered_legacy_registry_is_rendered_in_canonical_order():
registry = """| # | Название | Статус | P | Репо | Бюджет |
|---|---|---|---|---|---|
| 34 | **Обновление FMT-exocortex-template** | ⏳ | P3 | FMT-exocortex-template | 1h |
"""

rows, problems = BUILD.parse_registry(registry)

assert problems == []
assert rows[0]["project"] == "P3"
assert rows[0]["name"] == "**Обновление FMT-exocortex-template**"
assert rows[0]["status"] == "⏳"
assert rows[0]["repo"] == "FMT-exocortex-template"
assert "| 34 | P3 | **Обновление FMT-exocortex-template** | ⏳ | FMT-exocortex-template | 1h |" in BUILD.render(rows)


def test_minimal_legacy_registry_keeps_active_row():
registry = """| # | Название | Статус |
|---|---|---|
| 7 | Старый РП | 🔄 |
"""

rows, problems = BUILD.parse_registry(registry)

assert problems == []
assert rows[0]["project"] == "—"
assert rows[0]["status"] == "🔄"
assert "## 🔄 Открытые (1)" in BUILD.render(rows)


def test_wp_prefixed_legacy_identifier_is_not_dropped():
registry = """| # | Название | Статус |
|---|---|---|
| ~~WP-33~~ | ~~Закрытый РП~~ | ✅ |
"""

rows, problems = BUILD.parse_registry(registry)

assert problems == []
assert rows[0]["wp"] == 33
assert rows[0]["status"] == "✅"
assert "## 🔄 Открытые (0)" in BUILD.render(rows)
assert "📦 Закрытые (1)" in BUILD.render(rows)


def test_legacy_revision_identifiers_are_not_dropped():
registry = """| # | Название | Статус |
|---|---|---|
| WP-9-r3 | Актуальная ревизия | 🔄 |
| ~~WP-9-r2~~ | ~~Закрытая ревизия~~ | ✅ |
| ~~WP-1.2~~ | ~~Отменённая ревизия~~ | ❌ |
"""

rows, problems = BUILD.parse_registry(registry)
rendered = BUILD.render(rows)

assert problems == []
assert [row["wp"] for row in rows] == [9, 9, 1]
assert "| WP-9-r3 |" in rendered
assert "| ~~WP-9-r2~~ |" in rendered
assert "| ~~WP-1.2~~ |" in rendered
assert "## 🔄 Открытые (1)" in rendered
assert "📦 Закрытые (2)" in rendered
5 changes: 4 additions & 1 deletion seed/strategy/scripts/day-open-scaffold.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ IWE="$(iwe_resolve_root)"
IWE_ROOT="$IWE"
export IWE_ROOT IWE
DATE="${1:-$(date +%Y-%m-%d)}"
CONFIG="$IWE/${IWE_GOVERNANCE_REPO:-DS-strategy}/exocortex/day-rhythm-config.yaml"
CONFIG="$IWE/${IWE_GOVERNANCE_REPO:-DS-strategy}/day-rhythm-config.yaml"
if [ ! -f "$CONFIG" ]; then
CONFIG="$IWE/${IWE_GOVERNANCE_REPO:-DS-strategy}/exocortex/day-rhythm-config.yaml"
fi
PARAMS_FILE="$IWE/params.yaml"
MULTIPLIER_ENABLED="true"
if [ -f "$PARAMS_FILE" ] && grep -qE '^multiplier_enabled:[[:space:]]*false([[:space:]]*(#.*)?)?$' "$PARAMS_FILE"; then
Expand Down
5 changes: 3 additions & 2 deletions update-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1801,7 +1801,7 @@
},
{
"path": "scripts/build-active-wp.py",
"sha256": "ab896aa1b981955bc42daa507abc44da084a0d0328a49dfc73c2c4d19341d974"
"sha256": "a0c8516167c5aa8b67fcb8eb9d460898940c3d9d616e9b261420973b08d4d592"
},
{
"path": "scripts/changelog-append.sh",
Expand Down Expand Up @@ -1929,7 +1929,7 @@
},
{
"path": "scripts/day-open-scaffold.sh",
"sha256": "cc6ba46e9ff10ed40e6b334483f798f565e098119cbeb1cfbd623500e3b121d5"
"sha256": "b18329a4284f70bacce1b34d22d2cc381ae0bccfdafa35e1a8aeb2b40dbc46fb"
},
{
"path": "scripts/day-open-smoke-extended.sh",
Expand Down Expand Up @@ -2418,6 +2418,7 @@
"scripts/session-dispatcher-tsekh.py",
"scripts/tests/conftest.py",
"scripts/tests/test_agent_dashboard.py",
"scripts/tests/test_build_active_wp_registry_columns.py",
"scripts/tests/test_copy_to_aisystant.py",
"scripts/tests/test_create_wp_number_padding.py",
"scripts/tests/test_create_wp_weekplan_writer.py",
Expand Down
Loading