fix(project-rules): qualify ad-hoc record identifiers
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02b22-9638-76d2-bbff-b7ea1770b118
This commit is contained in:
tegwick 2026-08-23 14:45:26 +02:00
parent fae2816199
commit 284e28112d
6 changed files with 83 additions and 15 deletions

View file

@ -204,9 +204,12 @@ read/cache/index layer that rebuilds from files.
the completion/archive date; the frontmatter `id` does not change.
**Ad Hoc Tasks:** small opportunistic fixes discovered during a session use
`workplans/ADHOC-YYYY-MM-DD.md` with task ids `ADHOC-YYYY-MM-DD-T01`, etc. Use
this only for low-risk work completed directly; create a normal workplan for
anything needing analysis, design, approval, dependencies, or multiple phases.
`workplans/ADHOC-YYYY-MM-DD.md`, workplan id
`STATE-WP-ADHOC-YYYY-MM-DD`, and task ids
`STATE-WP-ADHOC-YYYY-MM-DD-T01`, etc. Unqualified historic `ADHOC-*` ids are
grandfathered and must not be copied into new records. Use this only for
low-risk work completed directly; create a normal workplan for anything needing
analysis, design, approval, dependencies, or multiple phases.
**Frontmatter:**

View file

@ -150,9 +150,13 @@ read/cache/index layer that rebuilds from files.
the completion/archive date; the frontmatter `id` does not change.
**Ad Hoc Tasks:** small opportunistic fixes discovered during a session use
`workplans/ADHOC-YYYY-MM-DD.md` with task ids `ADHOC-YYYY-MM-DD-T01`, etc. Use
this only for low-risk work completed directly; create a normal workplan for
anything needing analysis, design, approval, dependencies, or multiple phases.
`workplans/ADHOC-YYYY-MM-DD.md`, workplan id
`{WP_PREFIX}-ADHOC-YYYY-MM-DD`, and task ids
`{WP_PREFIX}-ADHOC-YYYY-MM-DD-T01`, etc. `{WP_PREFIX}` includes its final `-WP`
token. Unqualified historic `ADHOC-*` ids are grandfathered and must not be
copied into new records. Use this only for low-risk work completed directly;
create a normal workplan for anything needing analysis, design, approval,
dependencies, or multiple phases.
**Frontmatter:**

View file

@ -16,10 +16,13 @@ prefix: `YYMMDD-{WP_PREFIX}-NNNN-<slug>.md`. The frontmatter id remains
unchanged; the prefix is only for quick visual reference.
Small opportunistic tasks discovered during another session use **Ad Hoc Tasks**:
`workplans/ADHOC-YYYY-MM-DD.md`, workplan slug `adhoc-YYYY-MM-DD`, and task ids
`ADHOC-YYYY-MM-DD-T01`, `T02`, etc. Use adhocs only for low-risk work completed
directly. Promote anything requiring analysis, design, approval, dependencies, or
multiple planned phases into a normal workplan.
`workplans/ADHOC-YYYY-MM-DD.md`, workplan id
`{WP_PREFIX}-ADHOC-YYYY-MM-DD`, and task ids
`{WP_PREFIX}-ADHOC-YYYY-MM-DD-T01`, `T02`, etc. `{WP_PREFIX}` includes its final
`-WP` token. Unqualified historic `ADHOC-*` ids are grandfathered and must not
be copied into new records. Use adhocs only for low-risk work completed directly.
Promote anything requiring analysis, design, approval, dependencies, or multiple
planned phases into a normal workplan.
Ecosystem todos from other agents arrive as `[repo:{REPO_SLUG}]` hub tasks —
visible at session start. Pick one up by creating the workplan file, committing,

View file

@ -74,8 +74,15 @@ SUPPORTED_WP_STATUSES = set(SUPPORTED_WORKSTREAM_STATUSES)
VALID_TASK_STATUSES = set(CANONICAL_TASK_STATUSES)
VALID_TASK_PRIORITIES = {"low", "medium", "high", "critical"}
_WP_ID_RE = re.compile(r"^(?:[A-Z]+-WP-\d+|ADHOC-\d{4}-\d{2}-\d{2})$")
_TASK_ID_RE = re.compile(r"^(?:[A-Z]+-WP-\d+|ADHOC-\d{4}-\d{2}-\d{2})-T\d+$")
_PREFIX_RE = r"[A-Z][A-Z0-9]*(?:-[A-Z][A-Z0-9]*)*"
_WP_ID_RE = re.compile(
rf"^(?:{_PREFIX_RE}-WP-\d+|{_PREFIX_RE}-WP-ADHOC-\d{{4}}-\d{{2}}-\d{{2}}"
r"|ADHOC-\d{4}-\d{2}-\d{2})$"
)
_TASK_ID_RE = re.compile(
rf"^(?:{_PREFIX_RE}-WP-\d+|{_PREFIX_RE}-WP-ADHOC-\d{{4}}-\d{{2}}-\d{{2}}"
r"|ADHOC-\d{4}-\d{2}-\d{2})-T\d+$"
)
_TASK_BLOCK_RE = re.compile(r"```task\s*\n(.*?)\n```", re.DOTALL)
_ARCHIVED_WP_RE = re.compile(r"^\d{6}-(.+\.md)$")
@ -223,12 +230,18 @@ def _check_workplan_file(wp_file: Path, report: Report) -> dict | None:
wp_id = str(meta.get("id", ""))
if not _WP_ID_RE.match(wp_id):
report.add(Level.FAIL, "frontmatter-id-format",
f"id must match [A-Z]+-WP-\\d+ (e.g. CUST-WP-0001), got {wp_id!r}", fname)
"id must be a numbered or repository-qualified ad-hoc "
f"workplan id, got {wp_id!r}", fname)
else:
report.add(Level.PASS, "frontmatter-id-format", f"id={wp_id}", fname)
# filename prefix
if wp_id and not canonical_fname.startswith(wp_id):
adhoc_filename_id = Path(canonical_fname).stem
qualified_adhoc_filename = (
adhoc_filename_id.startswith("ADHOC-")
and wp_id.endswith(f"-WP-{adhoc_filename_id}")
)
if wp_id and not canonical_fname.startswith(wp_id) and not qualified_adhoc_filename:
report.add(Level.WARN, "filename-id-prefix",
f"Filename should start with id '{wp_id}', got {fname!r}", fname)
elif wp_id:
@ -260,7 +273,7 @@ def _check_workplan_file(wp_file: Path, report: Report) -> dict | None:
report.add(Level.FAIL, "task-id", "Missing 'id' field", tref)
elif not _TASK_ID_RE.match(t_id):
report.add(Level.WARN, "task-id-format",
f"id {t_id!r} doesn't match [A-Z]+-WP-\\d+-T\\d+", tref)
f"id {t_id!r} is not a canonical or grandfathered task id", tref)
t_status = str(task.get("status", ""))
if not t_status:

View file

@ -0,0 +1,36 @@
from __future__ import annotations
from scripts.validate_repo_adr import Report, _check_workplan_file
def test_repository_qualified_ad_hoc_id_matches_daily_filename(tmp_path) -> None:
workplans = tmp_path / "workplans"
workplans.mkdir()
path = workplans / "ADHOC-2026-08-23.md"
path.write_text(
"""---
id: ACTIVITY-WP-ADHOC-2026-08-23
type: workplan
title: Daily repair
domain: infotech
status: finished
owner: codex
created: "2026-08-23"
---
## Repair
```task
id: ACTIVITY-WP-ADHOC-2026-08-23-T01
status: done
priority: low
```
""",
encoding="utf-8",
)
report = Report(repo_path=str(tmp_path))
_check_workplan_file(path, report)
assert not report.failures
assert not [finding for finding in report.warnings if finding.check == "filename-id-prefix"]

View file

@ -356,6 +356,15 @@ that omit `domain_slug`. Two regression tests cover both paths; all 131
consistency-check tests pass. This is a compatibility fix until the generator
moves or retires, not new permanent State Hub authority.
**Ad-hoc identity compatibility repair (2026-08-23):** the surviving project
instruction templates prescribed fleet-wide `ADHOC-YYYY-MM-DD` identifiers.
Activity Core and Net Kingdom used that form on the same day, and deterministic
derivation correctly refused the second record. Templates and the legacy ADR
validator now accept and prescribe `{WP_PREFIX}-ADHOC-YYYY-MM-DD` while keeping
the daily filename unchanged. A regression test covers the filename/id pairing.
This prevents new collisions without making State Hub the long-term convention
owner.
## Retire legacy surfaces
```task