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:
parent
ccb285fc40
commit
f767a1a245
4 changed files with 231 additions and 17 deletions
|
|
@ -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
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
64
workplans/STATE-WP-0090-projection-task-field-convergence.md
Normal file
64
workplans/STATE-WP-0090-projection-task-field-convergence.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue