fix: preserve source task sections and explicit empty notes

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-10 15:11:03 +02:00
parent ccb285fc40
commit f767a1a245
4 changed files with 231 additions and 17 deletions

View file

@ -30,7 +30,7 @@ from api.services.task_record_id_backfill import qualify_task_id
_WORK_RECORD_NAMESPACE = uuid.UUID("a4058507-5c4a-5a00-ab06-fffa4fb46009")
_TASK_BLOCK_RE = re.compile(r"```task\s*\n(.*?)\n```", re.DOTALL)
_HEADING_RE = re.compile(r"^(#{1,4})\s+(.+?)$", re.MULTILINE)
_HEADING_RE = re.compile(r"^(#{1,6})\s+(.+?)$", re.MULTILINE)
DEFAULT_FORGE_BASE = "https://forgejo.coulomb.social/coulomb"
@ -272,12 +272,23 @@ def _split_frontmatter(text: str) -> tuple[dict, str]:
return (meta if isinstance(meta, dict) else {}), body
def _clean_task_description(raw: str) -> str | None:
"""Match the file-authoritative consistency parser's section boundaries."""
lines = raw.splitlines()
while lines and (not lines[0].strip() or lines[0].strip() in {"---", "***", "___"}):
lines.pop(0)
while lines and (not lines[-1].strip() or lines[-1].strip() in {"---", "***", "___"}):
lines.pop()
return "\n".join(lines).strip() or None
def _parse_tasks(body: str, workplan_id: str) -> list[DerivedTask]:
headings = [
(m.start(), m.group(2).strip()) for m in _HEADING_RE.finditer(body)
(m.start(), len(m.group(1)), m.group(2).strip()) for m in _HEADING_RE.finditer(body)
]
out: list[DerivedTask] = []
for m in _TASK_BLOCK_RE.finditer(body):
task_matches = list(_TASK_BLOCK_RE.finditer(body))
for index, m in enumerate(task_matches):
try:
block = yaml.safe_load(m.group(1).strip()) or {}
except yaml.YAMLError:
@ -287,15 +298,21 @@ def _parse_tasks(body: str, workplan_id: str) -> list[DerivedTask]:
rid = str(block.get("id") or "").strip()
if not rid:
continue
title = block.get("title")
if not title:
prev = [t for pos, t in headings if pos < m.start()]
title = prev[-1] if prev else None
following_headings = [pos for pos, _title in headings if pos > m.end()]
description_end = min(following_headings) if following_headings else len(body)
description = str(block.get("description") or "").strip()
if not description:
description = body[m.end() : description_end].strip()
prev = [(pos, level, text) for pos, level, text in headings if pos < m.start()]
heading = prev[-1] if prev else None
title = block.get("title") or (heading[2] if heading else None)
# Nested evidence belongs to this task. Stop at the next peer/ancestor
# heading or task block, as the ordinary consistency path does.
description_end = task_matches[index + 1].start() if index + 1 < len(task_matches) else len(body)
following_heading = next((
pos for pos, level, _text in headings
if pos > m.end() and (heading is None or level <= heading[1])
), None)
if following_heading is not None:
description_end = min(description_end, following_heading)
description = _clean_task_description(body[m.end():description_end])
if description is None and block.get("description") is not None:
description = str(block["description"])
out.append(
DerivedTask(
# A bare `T01` is not an identifier: it is unique only within
@ -315,16 +332,16 @@ def _parse_tasks(body: str, workplan_id: str) -> list[DerivedTask]:
title=title,
status=(str(block["status"]).strip() if block.get("status") else None),
priority=(str(block["priority"]).strip() if block.get("priority") else None),
description=description or None,
description=description,
needs_human=bool(block.get("needs_human", False)),
intervention_note=(
str(block["intervention_note"]).strip()
if block.get("intervention_note")
str(block["intervention_note"])
if block.get("intervention_note") is not None
else None
),
blocking_reason=(
str(block["blocking_reason"]).strip()
if block.get("blocking_reason")
str(block["blocking_reason"])
if block.get("blocking_reason") is not None
else None
),
)

View file

@ -17,6 +17,81 @@ from api.services import forge_credential as fc
from api.services import forge_projection as fp
def test_task_description_retains_nested_sections_and_matches_consistency(monkeypatch):
monkeypatch.syspath_prepend(str(Path(__file__).parents[1] / "scripts"))
from scripts.consistency_check import parse_task_blocks
body = '''## Factory admission
```task
id: HFACT-WP-0001-T01
status: progress
description: Earlier inline summary.
intervention_note: ""
blocking_reason: ""
```
---
Current native admission requirements.
### Local proof
Retain the source evidence and limitations.
#### Remaining return
Exact operator binding remains open.
---
## Next task
```task
id: HFACT-WP-0001-T02
status: wait
```
Separate next-task description.
'''
derived = fp._parse_tasks(body, "HFACT-WP-0001")
authoritative = parse_task_blocks(body)
assert derived[0].description == authoritative[0]["description"]
assert "### Local proof" in derived[0].description
assert "#### Remaining return" in derived[0].description
assert "Next task" not in derived[0].description
assert not derived[0].description.startswith("---")
assert derived[0].intervention_note == ""
assert derived[0].blocking_reason == ""
assert derived[1].description == authoritative[1]["description"]
assert derived[1].intervention_note is None
@pytest.mark.parametrize("heading", ["", "# Workplan\n", "## Task\n", "### Task\n"])
def test_task_description_stops_before_next_task_block(heading, monkeypatch):
monkeypatch.syspath_prepend(str(Path(__file__).parents[1] / "scripts"))
from scripts.consistency_check import parse_task_blocks
body = heading + '''```task
id: DEMO-WP-0001-T01
status: todo
```
First description.
```task
id: DEMO-WP-0001-T02
status: wait
description: Inline fallback.
intervention_note: null
blocking_reason: null
```
'''
derived = fp._parse_tasks(body, "DEMO-WP-0001")
authoritative = parse_task_blocks(body)
assert derived[0].description == authoritative[0]["description"] == "First description."
assert derived[1].description == authoritative[1]["description"] == "Inline fallback."
assert derived[1].intervention_note is derived[1].blocking_reason is None
def test_identity_matches_the_fleet_derivation():
"""Same namespace as ADR-007, so overlay and forge agree on identity."""
assert fp.derived_record_uuid("CUST-WP-0067") == "16249302-2767-55df-aec0-d92c2751c225"

View file

@ -146,3 +146,61 @@ async def test_reconcile_rejects_commit_mismatch_without_db_changes(
)
assert rows.status_code == 200
assert rows.json() == []
async def test_real_source_projection_converges_without_followup_task_patches(client, monkeypatch, tmp_path):
"""Derive source through the real parser and persist it through the real API/DB."""
await create_test_domain(client)
await create_test_repo(client, slug="projection-test")
from api.config import settings
from api.routers import work_record_projection as route
monkeypatch.setattr(settings, "state_hub_instance_role", "primary")
monkeypatch.setattr(settings, "state_hub_instance_label", "railiance01")
root = tmp_path / "projection-test"
(root / "workplans").mkdir(parents=True)
path = root / "workplans/TEST-WP-0001.md"
path.write_text('''---
id: TEST-WP-0001
type: workplan
title: Factory test
status: active
---
## Native admission
```task
id: TEST-WP-0001-T01
status: wait
intervention_note: Review needed.
blocking_reason: Waiting.
needs_human: true
```
Admission context.
### Proof
Retain full proof.
''')
commit = "a" * 40
monkeypatch.setattr(route, "derive_from_forge", lambda slug: fp.derive_from_checkout(root, slug, commit))
endpoint = "/repos/projection-test/work-record-projection/reconcile"
first = await client.post(endpoint, json={"expected_commit": commit}, headers={"Idempotency-Key": "source:first"})
assert first.status_code == 200, first.text
task_id = fp.derived_record_uuid("TEST-WP-0001-T01")
created = (await client.get(f"/tasks/{task_id}")).json()
assert created["description"] == "Admission context.\n\n### Proof\n\nRetain full proof."
assert created["needs_human"] is True
path.write_text(path.read_text().replace("Review needed.", '""').replace("Waiting.", '""').replace("needs_human: true", "needs_human: false"))
commit = "b" * 40
for key in ["source:updated", "source:independent-rederivation"]:
result = await client.post(endpoint, json={"expected_commit": commit}, headers={"Idempotency-Key": key})
assert result.status_code == 200, result.text
assert result.json()["outcome"]["status"] in {"applied", "noop"}
actual = (await client.get(f"/tasks/{task_id}")).json()
assert actual["description"] == created["description"]
assert actual["intervention_note"] == actual["blocking_reason"] == ""
assert actual["needs_human"] is False
if key == "source:independent-rederivation":
assert result.json()["outcome"]["counts"]["updated_tasks"] == 0

View file

@ -0,0 +1,64 @@
---
id: STATE-WP-0090
type: workplan
title: "Preserve task descriptions and explicit empty notes in Forge projection"
domain: infotech
repo: state-hub
status: active
owner: codex
topic_slug: infotech
created: "2026-09-10"
updated: "2026-09-10"
related: [STATE-WP-0086, RMGR-WP-0017, HFACT-WP-0001]
---
HFACT-WP-0001-T02 repeatedly repairs six of 24 selected task projections after
normal sync. Forge derivation truncates nested evidence and converts explicit
empty intervention notes into null, disagreeing with file-authoritative deep
consistency. Fix the original parser rather than applying another batch patch.
## Reproduce and correct task-field derivation
```task
id: STATE-WP-0090-T01
status: done
priority: high
```
Retain nested task sections, stop at the next peer/ancestor heading or task block,
match source-description cleanup and preserve absent/null/empty metadata meaning.
Keep task identity, status, collision and retirement rules unchanged. Test parity
with the existing consistency parser and real PostgreSQL/API convergence.
## Promote the verified correction and prove the selected chain
```task
id: STATE-WP-0090-T02
status: todo
priority: high
```
Run relevant projection/consistency/API regressions, commit/push, consume the
immutable CI image and prepare the existing Helm release with reused values and
atomic rollback. Verify the exact promoted image and healthy primary, then sync
the three selected repositories using RMGR-WP-0017 and read back all 24 task
fields with zero manual task PATCH repairs. Preserve unrelated untracked evidence.
## Retain evidence and close the bounded defect
```task
id: STATE-WP-0090-T03
status: todo
priority: medium
```
Record source/image/Helm revisions, test results and before/after convergence.
HFACT T02 retains other selected-backlog readiness work; native admission and
factory execution remain their existing owner tasks. Do not mark factory G1/G2
complete or introduce another synchronization service.
Validation: all five new parser cases fail on the previous derivation. The
corrected projection/consistency/API suite passes 242 tests on a disposable
PostgreSQL database. Actual selected-source comparison reproduces six drifted
tasks before the fix and zero afterward across 24 tasks. No production data
was used by the tests; live promotion and readback remain T02.