Verify offline scaffolds and preserve Forge qualifier receiving
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 29s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a10391-6355-7b41-b0fa-3e14e874f091
This commit is contained in:
tegwick 2026-10-04 02:32:37 +02:00
parent cd9dad05e8
commit 3791e608ba
5 changed files with 417 additions and 14 deletions

View file

@ -67,6 +67,7 @@
| workplan | STATE-WP-0091 | finished | — | workplans/STATE-WP-0091-release-headroom.md |
| workplan | STATE-WP-0092 | finished | — | workplans/STATE-WP-0092-workplan-flavor-and-depends-on-policy.md |
| workplan | STATE-WP-0093 | finished | — | workplans/STATE-WP-0093-broadcast-receipts-and-standing-notices.md |
| workplan | STATE-WP-0094 | active | — | workplans/STATE-WP-0094-routed-gates-and-owner-returns.md |
| task | STATE-WP-ADHOC-2026-06-04-T01 | done | — | workplans/ADHOC-2026-06-04.md |
| task | STATE-WP-ADHOC-2026-07-01-T01 | done | — | workplans/ADHOC-2026-07-01.md |
| task | STATE-WP-ADHOC-2026-07-01-T02 | done | — | workplans/ADHOC-2026-07-01.md |
@ -374,3 +375,8 @@
| task | STATE-WP-0093-T06 | done | — | workplans/STATE-WP-0093-broadcast-receipts-and-standing-notices.md |
| task | STATE-WP-0093-T07 | done | — | workplans/STATE-WP-0093-broadcast-receipts-and-standing-notices.md |
| task | STATE-WP-0093-T08 | done | — | workplans/STATE-WP-0093-broadcast-receipts-and-standing-notices.md |
| task | STATE-WP-0094-T01 | done | — | workplans/STATE-WP-0094-routed-gates-and-owner-returns.md |
| task | STATE-WP-0094-T02 | done | — | workplans/STATE-WP-0094-routed-gates-and-owner-returns.md |
| task | STATE-WP-0094-T03 | todo | — | workplans/STATE-WP-0094-routed-gates-and-owner-returns.md |
| task | STATE-WP-0094-T04 | done | — | workplans/STATE-WP-0094-routed-gates-and-owner-returns.md |
| task | STATE-WP-0094-T05 | todo | — | workplans/STATE-WP-0094-routed-gates-and-owner-returns.md |

View file

@ -19,6 +19,7 @@ import subprocess
import tempfile
import uuid
from dataclasses import dataclass, field
from datetime import date
from pathlib import Path
from typing import Any
@ -27,6 +28,10 @@ import yaml
# Same derivation as ADR-007 / repo-manager, so a forge-derived projection and a
# preliminary overlay compute identical identities for the same record.
from api.services.task_record_id_backfill import qualify_task_id
from api.work_record_flavor import (
FLAVOR_PROMOTION_REASONS, WORK_RECORD_FLAVORS,
normalize_flavor, normalize_promotion_reason,
)
_WORK_RECORD_NAMESPACE = uuid.UUID("a4058507-5c4a-5a00-ab06-fffa4fb46009")
_TASK_BLOCK_RE = re.compile(r"```task\s*\n(.*?)\n```", re.DOTALL)
@ -129,6 +134,10 @@ class DerivedTask:
needs_human: bool = False
intervention_note: str | None = None
blocking_reason: str | None = None
flavor: str | None = None
decision_id: str | None = None
blocked_on: str | None = None
wait_until: date | None = None
@dataclass
@ -142,6 +151,9 @@ class DerivedWorkplan:
owner: str | None = None
description: str | None = None
tasks: list[DerivedTask] = field(default_factory=list)
flavor: str | None = None
flavor_promotion_reason: str | None = None
flavor_promoted_from: str | None = None
@dataclass
@ -188,6 +200,9 @@ class DerivedProjection:
"status": w.status,
"owner": w.owner,
"description": w.description,
"flavor": w.flavor,
"flavor_promotion_reason": w.flavor_promotion_reason,
"flavor_promoted_from": w.flavor_promoted_from,
"relative_path": w.relative_path,
"archived": w.archived,
"tasks": [
@ -201,6 +216,10 @@ class DerivedProjection:
"needs_human": t.needs_human,
"intervention_note": t.intervention_note,
"blocking_reason": t.blocking_reason,
"flavor": t.flavor,
"decision_id": t.decision_id,
"blocked_on": t.blocked_on,
"wait_until": t.wait_until.isoformat() if t.wait_until else None,
}
for t in w.tasks
],
@ -282,6 +301,56 @@ def _clean_task_description(raw: str) -> str | None:
return "\n".join(lines).strip() or None
def _source_flavor(value: Any, field_name: str = "flavor") -> str | None:
flavor = normalize_flavor(value)
if flavor is not None and flavor not in WORK_RECORD_FLAVORS:
raise ForgeDeriveError(f"Unknown source {field_name}; no projection was applied")
return flavor
def _source_promotion_reason(value: Any) -> str | None:
reason = normalize_promotion_reason(value)
if reason is not None and reason not in FLAVOR_PROMOTION_REASONS:
raise ForgeDeriveError("Unknown source flavor_promotion_reason; no projection was applied")
return reason
def _source_wait_until(value: Any) -> date | None:
if value is None or value == "":
return None
if type(value) is date:
return value
text = str(value).strip()
if re.fullmatch(r"\d{4}-\d{2}-\d{2}", text):
try:
return date.fromisoformat(text)
except ValueError:
pass
raise ForgeDeriveError("Source wait_until must be a valid YYYY-MM-DD date; no projection was applied")
def _source_wait_text(value: Any) -> str | None:
if value is None:
return None
if not isinstance(value, str):
raise ForgeDeriveError("Source wait qualifier must be text; no projection was applied")
return value.strip() or None
def _source_needs_human(value: Any) -> bool:
if isinstance(value, bool):
return value
if value is None:
return False
text = str(value).strip().lower()
if text in {"true", "yes", "1", "on"}:
return True
if text in {"false", "no", "0", "off", ""}:
return False
raise ForgeDeriveError("Source needs_human must be boolean; no projection was applied")
def _parse_tasks(body: str, workplan_id: str) -> list[DerivedTask]:
headings = [
(m.start(), len(m.group(1)), m.group(2).strip()) for m in _HEADING_RE.finditer(body)
@ -291,8 +360,10 @@ def _parse_tasks(body: str, workplan_id: str) -> list[DerivedTask]:
for index, m in enumerate(task_matches):
try:
block = yaml.safe_load(m.group(1).strip()) or {}
except yaml.YAMLError:
continue
except (yaml.YAMLError, ValueError) as exc:
# PyYAML raises ValueError for impossible bare ISO dates.
# Refuse the entire derive instead of dropping a stored gate.
raise ForgeDeriveError("Malformed source task metadata; no projection was applied") from exc
if not isinstance(block, dict):
continue
rid = str(block.get("id") or "").strip()
@ -333,17 +404,21 @@ def _parse_tasks(body: str, workplan_id: str) -> list[DerivedTask]:
status=(str(block["status"]).strip() if block.get("status") else None),
priority=(str(block["priority"]).strip() if block.get("priority") else None),
description=description,
needs_human=bool(block.get("needs_human", False)),
needs_human=_source_needs_human(block.get("needs_human", False)),
intervention_note=(
str(block["intervention_note"])
if block.get("intervention_note") is not None
else None
else (str(block["blocking_reason"]) if _source_needs_human(block.get("needs_human")) and block.get("blocking_reason") is not None else None)
),
blocking_reason=(
str(block["blocking_reason"])
if block.get("blocking_reason") is not None
else None
),
flavor=_source_flavor(block.get("flavor")),
decision_id=_source_wait_text(block.get("decision_id")),
blocked_on=_source_wait_text(block.get("blocked_on")),
wait_until=_source_wait_until(block.get("wait_until")),
)
)
return out
@ -397,6 +472,9 @@ def derive_from_checkout(repo_root: Path, repo_slug: str, commit: str) -> Derive
else _workplan_description(body)
),
tasks=_parse_tasks(body, rid),
flavor=_source_flavor(meta.get("flavor")),
flavor_promotion_reason=_source_promotion_reason(meta.get("flavor_promotion_reason")),
flavor_promoted_from=_source_flavor(meta.get("flavor_promoted_from"), "flavor_promoted_from"),
)
)
proj.workplans.sort(key=lambda w: w.record_id)
@ -611,6 +689,10 @@ def diff_against_hub(
changed = {}
if (cur.get("status") or None) != (w.status or None):
changed["status"] = {"hub": cur.get("status"), "forge": w.status}
for field_name in ("flavor", "flavor_promotion_reason", "flavor_promoted_from"):
expected = getattr(w, field_name)
if cur.get(field_name) != expected:
changed[field_name] = {"hub": cur.get(field_name), "forge": expected}
cur_path = cur.get("backing_relative_path") or None
if cur_path != w.relative_path:
changed["backing_relative_path"] = {"hub": cur_path, "forge": w.relative_path}
@ -674,11 +756,25 @@ def diff_against_hub(
)
for key, t in want.items():
cur = have.get(key)
if cur and (cur.get("status") or None) != (t.status or None):
if not cur:
continue
changed = {}
if (cur.get("status") or None) != (t.status or None):
changed["status"] = {"hub": cur.get("status"), "forge": t.status}
for field_name in ("needs_human", "intervention_note", "blocking_reason",
"flavor", "decision_id", "blocked_on", "wait_until"):
expected = getattr(t, field_name)
actual = cur.get(field_name, False if field_name == "needs_human" else None)
if isinstance(actual, date):
actual = actual.isoformat()
if isinstance(expected, date):
expected = expected.isoformat()
if actual != expected:
changed[field_name] = {"hub": actual, "forge": expected}
if changed:
d.differing.append(
{"kind": "task", "record_id": t.record_id, "uuid": t.uuid,
"workplan": w.record_id,
"changed": {"status": {"hub": cur.get("status"), "forge": t.status}}}
"workplan": w.record_id, "changed": changed}
)
return d
@ -822,6 +918,10 @@ def _sync_existing_workplan_tasks(
"needs_human": dt.needs_human,
"intervention_note": dt.intervention_note,
"blocking_reason": dt.blocking_reason,
"flavor": dt.flavor,
"decision_id": dt.decision_id,
"blocked_on": dt.blocked_on,
"wait_until": dt.wait_until,
}
st = _coerce_task_status(dt.status)
if st is not None:
@ -857,6 +957,11 @@ def _sync_existing_workplan_tasks(
if getattr(ht, "blocking_reason", None) != dt.blocking_reason:
ht.blocking_reason = dt.blocking_reason
changed = True
for field_name in ("flavor", "decision_id", "blocked_on", "wait_until"):
expected = getattr(dt, field_name)
if getattr(ht, field_name, None) != expected:
setattr(ht, field_name, expected)
changed = True
if dt.priority:
try:
task_priority = TaskPriority(dt.priority.strip().lower())
@ -1324,6 +1429,9 @@ async def reset_repository_projection(
description=w.description,
status=w.status or "proposed",
owner=w.owner,
flavor=w.flavor,
flavor_promotion_reason=w.flavor_promotion_reason,
flavor_promoted_from=w.flavor_promoted_from,
backing_filename=w.relative_path.rsplit("/", 1)[-1],
backing_relative_path=w.relative_path,
backing_archived=w.archived,
@ -1346,6 +1454,10 @@ async def reset_repository_projection(
needs_human=t.needs_human,
intervention_note=t.intervention_note,
blocking_reason=t.blocking_reason,
flavor=t.flavor,
decision_id=t.decision_id,
blocked_on=t.blocked_on,
wait_until=t.wait_until,
)
)
outcome.created.append(w.record_id)
@ -1372,6 +1484,11 @@ async def reset_repository_projection(
if w.status and row.status != w.status:
row.status = w.status
changed = True
for field_name in ("flavor", "flavor_promotion_reason", "flavor_promoted_from"):
expected = getattr(w, field_name)
if getattr(row, field_name, None) != expected:
setattr(row, field_name, expected)
changed = True
if row.backing_relative_path != w.relative_path:
row.backing_relative_path = w.relative_path
row.backing_filename = w.relative_path.rsplit("/", 1)[-1]

View file

@ -0,0 +1,63 @@
# STATE-WP-0094-T04 — Forge qualifier receiving source validation
The committed source model already supports task decision_id, blocked_on,
wait_until and flavor, and workplan flavor, flavor_promotion_reason and
flavor_promoted_from. Forge receiving omitted them. The correction carries them
through source derivation, projection JSON, dry-run diff, creation of a new
workplan/task, creation of a task on an existing workplan, and updates of existing
rows. It reuses the established closed flavor vocabulary and normalization.
Source authority is explicit: absent or YAML-null qualifiers/flavors clear stored
values; quoted false clears needs_human. Existing identities are retained.
Date-only values remain datetime.date in persistence and ISO dates in JSON.
Bare/quoted timestamps, impossible dates, malformed qualifier YAML, invalid
boolean/text qualifiers and unknown flavor values refuse derivation before
receiving can update or cancel stored gates. The existing ForgeDeriveError API
path reports the refusal. Origin and origin_ref are retained only in source;
no schema or migration was added. Activity Core historical shared identities
and frontmatter task parsing are outside this correction.
## Validation and retained failed trial
Actual source validation command, run alone because the fixture database has a
session-level schema lifecycle:
```sh
PYTHONDONTWRITEBYTECODE=1 .venv/bin/python -m pytest -p no:cacheprovider tests/test_forge_projection.py tests/test_forge_projection_qualifiers.py tests/test_work_record_projection_api.py tests/test_work_record_flavor.py tests/test_wait_kind.py -q
```
Final result: **130 passed in 41.68s**. `git diff --check` passes. Ruff is unavailable
in the repository venv and PATH; no lint result is claimed.
Earlier trial: **127 passed, two failed**. Both failures exposed PyYAML's plain
ValueError for the impossible bare date 2099-02-31. Catching YAML/ValueError as a
whole-derivation refusal fixed the regression. Review before that run also found
datetime accepted through isinstance(date); exact date acceptance and timestamp
regressions corrected it. These are refinements of one trial, not extra successes.
The real API/PostgreSQL regression proves residual creation and exclusion from
open views, promotion metadata, task creation on existing parents, qualifier
updates, source null/absent clearing, unchanged UUIDs, normalized quoted false,
independent repeat noop, and no stored-row changes after invalid source.
Tested SHA256 values:
- api/services/forge_projection.py: d935fc6c9cb34c5d340ac09f02c929c4244a5fb2b2cf44796972083e98c98516
- tests/test_forge_projection_qualifiers.py: a9c83c8b312f61e540d9f131c47c2adacabe219ae89644e480a9354668f27491
## Boundaries and remaining live work
This is local source correctness, not a production receiving receipt. T05 owns
current deployed revision, reviewed image/rollout/rollback preparation, separate
production authorization and primary parity through normal immutable Forge
reconciliation; direct C-41 repairs do not satisfy it. T05 also retains actual
graph edge/freshness coverage. T03 retains supported runner selection, pickup
deduplication and receipts; the execution launch-request consumer is retired.
No deployment, schedule or human decision resolution occurred.
The historical CLI symptom remains distinguished. Today's CLI uses the same
checker and deliberately turns warnings-only exit 2 into shell success. C-16
can skip writes, so successful process exit alone is not field parity. The prior
Forge receiving omitted new fields on creation and ignored them on existing
rows, including source removal; that does not prove it actively erased earlier
correct qualifiers or explain every historical CLI/direct-run discrepancy.

View file

@ -0,0 +1,172 @@
"""File-derived qualifiers survive normal receiving, including source clears."""
from datetime import date
from pathlib import Path
import pytest
from api.services import forge_projection as fp
from tests.conftest import create_test_domain, create_test_repo
def _source(tmp_path: Path, *, workplan_fields='', task_fields='', extra_fields=None):
root = tmp_path / 'projection-test'
path = root / 'workplans/TEST-WP-0001.md'
path.parent.mkdir(parents=True, exist_ok=True)
body = (
'---\nid: TEST-WP-0001\ntype: workplan\ntitle: Receiving test\nstatus: active\n'
+ workplan_fields + '---\n\n## Gate\n\n```task\nid: TEST-WP-0001-T01\n'
'status: wait\npriority: high\n' + task_fields + '```\n\nRetain the owner gate.\n'
)
if extra_fields is not None:
body += '\n## Second gate\n\n```task\nid: TEST-WP-0001-T02\nstatus: wait\n' + extra_fields + '```\n'
path.write_text(body)
return root, path
@pytest.mark.parametrize('value', ['2099-01-31', '"2099-01-31"'])
def test_derivation_and_json_retain_normalized_metadata(tmp_path, value):
root, _ = _source(tmp_path,
workplan_fields='flavor: " RESIDUAL "\nflavor_promotion_reason: " Risk "\nflavor_promoted_from: " Planning "\n',
task_fields='flavor: " IMPLEMENTATION "\nneeds_human: "false"\ndecision_id: " decision-1 "\n'
'blocked_on: " message-from:provider "\nwait_until: ' + value + '\n')
derived = fp.derive_from_checkout(root, 'projection-test', 'a' * 40)
workplan, task = derived.workplans[0], derived.workplans[0].tasks[0]
assert (workplan.flavor, workplan.flavor_promotion_reason, workplan.flavor_promoted_from) == ('residual', 'risk', 'planning')
assert task.needs_human is False
assert task.flavor == 'implementation'
assert task.decision_id == 'decision-1'
assert task.blocked_on == 'message-from:provider'
assert task.wait_until == date(2099, 1, 31)
payload = derived.to_dict()['workplans'][0]
assert payload['flavor'] == 'residual'
assert payload['flavor_promotion_reason'] == 'risk'
assert payload['flavor_promoted_from'] == 'planning'
assert payload['tasks'][0]['wait_until'] == '2099-01-31'
@pytest.mark.parametrize('field', [
'wait_until: not-a-date\n', 'wait_until: 2099-02-31\n', 'wait_until: [2099-01-31]\n',
'wait_until: 2099-01-31T12:30:00Z\n', 'wait_until: "2099-01-31T00:00:00Z"\n',
'needs_human: "perhaps"\n', 'decision_id: [decision-1]\n',
'blocked_on: {owner: provider}\n', 'flavor: invented\n',
'decision_id: [unclosed\n',
])
def test_invalid_task_metadata_refuses_derivation(tmp_path, field):
root, _ = _source(tmp_path, task_fields=field)
with pytest.raises(fp.ForgeDeriveError):
fp.derive_from_checkout(root, 'projection-test', 'a' * 40)
@pytest.mark.parametrize('field', ['flavor: invented\n', 'flavor_promoted_from: invented\n', 'flavor_promotion_reason: invented\n'])
def test_invalid_workplan_flavor_metadata_refuses_derivation(tmp_path, field):
root, _ = _source(tmp_path, workplan_fields=field)
with pytest.raises(fp.ForgeDeriveError):
fp.derive_from_checkout(root, 'projection-test', 'a' * 40)
def test_dry_run_diff_exposes_qualifier_and_flavor_drift(tmp_path):
root, _ = _source(tmp_path,
workplan_fields='flavor: residual\nflavor_promotion_reason: demand\nflavor_promoted_from: planning\n',
task_fields='flavor: implementation\nneeds_human: true\nblocking_reason: Owner must review.\n'
'decision_id: decision-1\nblocked_on: message-from:provider\nwait_until: 2099-01-31\n')
derived = fp.derive_from_checkout(root, 'projection-test', 'a' * 40)
workplan, task = derived.workplans[0], derived.workplans[0].tasks[0]
hub_workplan = {'id': workplan.uuid, 'slug': workplan.record_id.lower(), 'status': 'active',
'backing_relative_path': workplan.relative_path}
hub_task = {'id': task.uuid, 'record_id': task.record_id, 'status': 'wait', 'needs_human': False}
diff = fp.diff_against_hub(derived, [hub_workplan], {workplan.uuid: [hub_task]})
changes = {item['kind']: item['changed'] for item in diff.differing}
assert set(changes['workplan']) == {'flavor', 'flavor_promotion_reason', 'flavor_promoted_from'}
assert set(changes['task']) == {'flavor', 'needs_human', 'blocking_reason', 'intervention_note', 'decision_id', 'blocked_on', 'wait_until'}
assert changes['task']['wait_until']['forge'] == '2099-01-31'
# Diff is read-only, including the synthetic snapshot inputs.
assert hub_task['needs_human'] is False
assert 'flavor' not in hub_workplan
@pytest.mark.asyncio
async def test_real_receiving_creates_updates_and_clears_source_metadata(client, monkeypatch, tmp_path):
await create_test_domain(client)
repo = 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, _ = _source(tmp_path,
workplan_fields='flavor: residual\n',
task_fields='flavor: residual\nneeds_human: true\nblocking_reason: Founder must review.\n'
'decision_id: decision-1\nblocked_on: message-from:provider\nwait_until: 2099-01-31\n')
revision = ['a' * 40]
monkeypatch.setattr(route, 'derive_from_forge', lambda slug: fp.derive_from_checkout(root, slug, revision[0]))
endpoint = '/repos/projection-test/work-record-projection/reconcile'
async def reconcile(key):
response = await client.post(endpoint, json={'expected_commit': revision[0]}, headers={'Idempotency-Key': key})
assert response.status_code == 200, response.text
return response.json()['outcome']
async def snapshot():
response = await client.get('/repos/projection-test/work-record-projection/snapshot')
assert response.status_code == 200
return response.json()
first = await reconcile('qualifiers:create')
assert first['status'] == 'applied'
data = await snapshot()
workplan = data['workplans'][0]
task = data['tasks'][0]
assert workplan['flavor'] == task['flavor'] == 'residual'
assert task['needs_human'] is True
assert task['intervention_note'] == 'Founder must review.'
assert task['decision_id'] == 'decision-1'
assert task['blocked_on'] == 'message-from:provider'
assert task['wait_until'] == '2099-01-31'
hidden = (await client.get('/workplans/', params={'repo_id': repo['id'], 'include_residuals': 'false'})).json()
assert hidden == []
summary = (await client.get('/state/summary')).json()
assert workplan['id'] not in {row['id'] for row in summary['open_workplans']}
# Existing workplan, existing task, and a new task on that existing parent.
_source(tmp_path,
workplan_fields='flavor: implementation\nflavor_promotion_reason: risk\nflavor_promoted_from: residual\n',
task_fields='flavor: extension\nneeds_human: true\nblocking_reason: Founder must review.\n'
'decision_id: decision-2\nblocked_on: message-from:other\nwait_until: 2099-02-01\n',
extra_fields='flavor: residual\ndecision_id: decision-3\nblocked_on: message-from:new-owner\nwait_until: 2099-03-01\n')
revision[0] = 'b' * 40
assert (await reconcile('qualifiers:update'))['status'] == 'applied'
updated = await snapshot()
workplan = updated['workplans'][0]
tasks = {row['record_id']: row for row in updated['tasks']}
assert (workplan['flavor'], workplan['flavor_promotion_reason'], workplan['flavor_promoted_from']) == ('implementation', 'risk', 'residual')
task = tasks['TEST-WP-0001-T01']
assert (task['flavor'], task['decision_id'], task['blocked_on'], task['wait_until']) == ('extension', 'decision-2', 'message-from:other', '2099-02-01')
second = tasks['TEST-WP-0001-T02']
assert (second['flavor'], second['decision_id'], second['blocked_on'], second['wait_until']) == ('residual', 'decision-3', 'message-from:new-owner', '2099-03-01')
groups = {workplan['id']: list(tasks.values())}
diff = fp.diff_against_hub(fp.derive_from_checkout(root, 'projection-test', revision[0]), [workplan], groups)
assert diff.missing == diff.stale == diff.differing == []
# Omitting qualifiers clears them; explicit null also clears. Quoted false
# clears the human flag. Source identities survive every update.
_source(tmp_path, task_fields='needs_human: "false"\ndecision_id: null\n', extra_fields='wait_until: null\n')
revision[0] = 'c' * 40
assert (await reconcile('qualifiers:clear'))['status'] == 'applied'
cleared = await snapshot()
assert cleared['workplans'][0]['id'] == workplan['id']
for name in ('flavor', 'flavor_promotion_reason', 'flavor_promoted_from'):
assert cleared['workplans'][0][name] is None
assert {row['id'] for row in cleared['tasks']} == {row['id'] for row in updated['tasks']}
for row in cleared['tasks']:
assert row['needs_human'] is False
for name in ('flavor', 'decision_id', 'blocked_on', 'wait_until', 'blocking_reason', 'intervention_note'):
assert row[name] is None
assert (await reconcile('qualifiers:independent-rederivation'))['status'] == 'noop'
# Invalid source dates fail before receiving can replace/cancel stored gates.
_source(tmp_path, task_fields='wait_until: 2099-02-31\n', extra_fields='')
revision[0] = 'd' * 40
invalid = await client.post(endpoint, json={'expected_commit': revision[0]}, headers={'Idempotency-Key': 'qualifiers:invalid'})
assert invalid.status_code == 502
unchanged = await snapshot()
assert unchanged['workplans'] == cleared['workplans']
assert unchanged['tasks'] == cleared['tasks']

View file

@ -58,21 +58,25 @@ record receipts and provide an opt-in activation plan. Reuse the Custodian
dependency monitor and established runner; do not introduce another schedule.
Production activation remains a separately reviewed founder action.
## Diagnose qualifier projection parity (P6a)
## Correct Forge qualifier receiving in source (P6a)
```task
id: STATE-WP-0094-T04
status: todo
status: done
priority: high
state_hub_task_id: "cd03399c-a905-5804-9700-e5e763d2500b"
```
Investigate report 70d08f35-a008-4b2c-878d-d9f32db892ce with the current editable
CLI and direct checker from alternate CWDs. Separate missing canonical record_id
from real UUID-addressed qualifier drift. Retain the historical report even if
current source does not reproduce it; correct only an evidenced defect.
Correct the evidenced immutable Forge receiving omission: task decision_id,
blocked_on, wait_until and flavor, and workplan flavor/promotion fields were
absent from derivation, serialization, diff and create/update receiving paths.
Verify source normalization, refusal of malformed qualifiers before receiving,
creation, updates, deliberate null/absent clears and repeat noop against the real
API and PostgreSQL test database. This task completes source correctness only;
historical CLI report 70d08f35-a008-4b2c-878d-d9f32db892ce remains separate from
that evidenced omission. Primary deployment and normal-sync parity remain T05.
## Assess nightly graph maintenance against the Forge boundary (P6)
## Prepare approved receiving promotion and assess graph freshness (P6)
```task
id: STATE-WP-0094-T05
@ -110,3 +114,44 @@ this form. Production Helm source explicitly disables it pending RMGR-WP-0005-T1
the supported alternative is immutable Forge reconciliation (STATE-WP-0086)
and a separately reviewed freshness/wakeup runner under T03/T05. No scheduler,
deployment, credential or human decision was changed.
## Source validation receipt
2026-10-04 T04 source correction verified: 130 receiving, flavor and wait-kind
tests pass in the serial real API/PostgreSQL suite; whitespace check passes.
The earlier trial had 127 passing and two failing tests: PyYAML raised ValueError
for an impossible bare date before the date validator. The refinement converts
both invalid-date and malformed-task-YAML errors into a derivation refusal,
preserving existing gates. A review also identified datetime being a date
subclass; exact date acceptance plus bare/quoted timestamp regressions now
enforce YYYY-MM-DD. These refinements remain one correction trial.
Evidence: [Forge qualifier receiving](../docs/evidence/2026-10-04-forge-qualifier-receiving.md).
Origin/origin_ref remain in source because the workplan model has no such fields.
No migration, production deployment or schedule was performed. Ruff was absent
from the venv and PATH; no lint success is claimed.
T05 stays live: identify the actual deployed receiving revision, prepare a
concrete reviewed image/rollout/rollback package, obtain the separate per-change
production authorization, then prove primary qualifier/flavor/null-clear parity
through normal immutable Forge reconciliation without follow-up field PATCHes.
Point-in-time direct C-41 repairs do not supply that receiving proof. Confirm
task-level graph edge support and actual supported freshness coverage as well.
T03 stays live: api/services/execution_queue.py rejects new launch requests and
has no launch-request pickup consumer. Select the supported owner-agent runner,
stable task/message deduplication and receipts through owner interfaces; do not
wire C-25 to the retired queue. Current chart sweep is disabled and legacy
hostPath fields are rollback-only. Activity Core owns schedules/ops runs, while
its runbook routes the C-rule engine to Repo Manager. Older live-sweep prose in
docs/activity-core-delegation.md does not establish current runtime coverage.
Historical CLI causality distinction: cmd_fix_consistency forwards the same
checker, --fix/path/API/no-writeback arguments and intentionally maps warnings
exit 2 to shell success. C-16 can skip writes despite that successful shell exit.
This does not isolate every historical CLI/direct-checker discrepancy. The old
Forge path omitted new fields on creation and left them untouched on existing
rows; it did not explicitly erase already-correct new qualifiers. Source removal
now deliberately clears them. Do not call the original CLI report or deployed
receiving fixed merely because this bounded source task is done.