feat: harden work-record and SBOM client contracts

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
This commit is contained in:
tegwick 2026-08-22 23:19:36 +02:00
parent 2577379e36
commit 84952c5212
16 changed files with 605 additions and 30 deletions

View file

@ -18,7 +18,7 @@
| workplan | RMGR-WP-0008 | finished | — | workplans/RMGR-WP-0008-work-record-and-register-receiving-surface.md | | workplan | RMGR-WP-0008 | finished | — | workplans/RMGR-WP-0008-work-record-and-register-receiving-surface.md |
| workplan | RMGR-WP-0009 | finished | — | workplans/RMGR-WP-0009-coding-assistant-commit-provenance.md | | workplan | RMGR-WP-0009 | finished | — | workplans/RMGR-WP-0009-coding-assistant-commit-provenance.md |
| workplan | RMGR-WP-0010 | finished | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | | workplan | RMGR-WP-0010 | finished | — | workplans/RMGR-WP-0010-authoritative-workload-references.md |
| workplan | RMGR-WP-0011 | ready | — | workplans/RMGR-WP-0011-sbom-nexus-production-client.md | | workplan | RMGR-WP-0011 | active | — | workplans/RMGR-WP-0011-sbom-nexus-production-client.md |
| task | RMGR-WP-0001-T01 | done | — | workplans/RMGR-WP-0001-foundation.md | | task | RMGR-WP-0001-T01 | done | — | workplans/RMGR-WP-0001-foundation.md |
| task | RMGR-WP-0001-T02 | done | — | workplans/RMGR-WP-0001-foundation.md | | task | RMGR-WP-0001-T02 | done | — | workplans/RMGR-WP-0001-foundation.md |
| task | RMGR-WP-0001-T03 | done | — | workplans/RMGR-WP-0001-foundation.md | | task | RMGR-WP-0001-T03 | done | — | workplans/RMGR-WP-0001-foundation.md |
@ -88,9 +88,9 @@
| task | RMGR-WP-0010-T05 | done | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | | task | RMGR-WP-0010-T05 | done | — | workplans/RMGR-WP-0010-authoritative-workload-references.md |
| task | RMGR-WP-0010-T06 | done | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | | task | RMGR-WP-0010-T06 | done | — | workplans/RMGR-WP-0010-authoritative-workload-references.md |
| task | RMGR-WP-0010-T07 | done | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | | task | RMGR-WP-0010-T07 | done | — | workplans/RMGR-WP-0010-authoritative-workload-references.md |
| task | RMGR-WP-0011-T01 | todo | — | workplans/RMGR-WP-0011-sbom-nexus-production-client.md | | task | RMGR-WP-0011-T01 | done | — | workplans/RMGR-WP-0011-sbom-nexus-production-client.md |
| task | RMGR-WP-0011-T02 | todo | — | workplans/RMGR-WP-0011-sbom-nexus-production-client.md | | task | RMGR-WP-0011-T02 | todo | — | workplans/RMGR-WP-0011-sbom-nexus-production-client.md |
| task | RMGR-WP-0011-T03 | todo | — | workplans/RMGR-WP-0011-sbom-nexus-production-client.md | | task | RMGR-WP-0011-T03 | done | — | workplans/RMGR-WP-0011-sbom-nexus-production-client.md |
| task | RMGR-WP-0011-T04 | todo | — | workplans/RMGR-WP-0011-sbom-nexus-production-client.md | | task | RMGR-WP-0011-T04 | todo | — | workplans/RMGR-WP-0011-sbom-nexus-production-client.md |
| intake | RMGR-IN-0001 | open | — | intakes/intakes.md | | intake | RMGR-IN-0001 | open | — | intakes/intakes.md |
| intake | RMGR-IN-0002 | open | — | intakes/intakes.md | | intake | RMGR-IN-0002 | open | — | intakes/intakes.md |

View file

@ -0,0 +1,44 @@
# Implementation snapshot of the Custodian work-record kind registry.
# Canonical source:
# the-custodian/canon/standards/work-record-types.yaml (version 0.1)
version: "0.1"
status: active
kinds:
- kind: workplan
id_patterns:
- "^[A-Z]+-WP-[0-9]{4}$"
legacy_patterns:
- pattern: "^MASON-0001$"
source: ops-mason bootstrap workplan created before WP infix convention
grandfathered: true
- kind: task
id_patterns:
- "^[A-Z]+-WP-[0-9]{4}-T[0-9]{2,3}$"
- "^ADHOC-[0-9]{4}-[0-9]{2}-[0-9]{2}-T[0-9]{2}$"
legacy_patterns:
- pattern: "^[A-Z]+-WP-[0-9]{4}-T[0-9]$"
source: early single-digit task suffixes (pre task-block convention)
grandfathered: true
- pattern: "^[A-Z]+-WP-[0-9]{4}-LEGACY-T[0-9]{2}$"
source: archived-workplan legacy task renumbering
grandfathered: true
- pattern: "^MASON-0001-T[0-9]{2,3}$"
source: ops-mason bootstrap tasks created before WP infix convention
grandfathered: true
- kind: intake
id_patterns:
- "^[A-Z]+-IN-[0-9]{4}$"
legacy_patterns:
- pattern: "^AWQ-[0-9]{3}$"
source: binky-control AutopilotWorkQueue
grandfathered: true
- kind: decision
id_patterns:
- "^[A-Z]+-DEC-[0-9]{4}-[0-9]{3}$"
legacy_patterns:
- pattern: "^DEC-[0-9]{4}-[0-9]{3}$"
source: binky-control DecisionQueue
grandfathered: true
identity_reconciliation:
repeated_same_id_same_uuid: duplicate_source_occurrence
repeated_same_id_different_uuid: identity_collision

View file

@ -74,6 +74,10 @@ SBOM Nexus is the sole scanner and snapshot product owner. Repo Manager retains
these aliases only so existing repository workflows do not break; set these aliases only so existing repository workflows do not break; set
`SBOM_NEXUS_CLI` when `sbom-nexus` is not on `PATH`. Their JSON includes `SBOM_NEXUS_CLI` when `sbom-nexus` is not on `PATH`. Their JSON includes
`product_owner: sbom-nexus` and the Nexus `sbom-nexus.snapshot.v1` schema. `product_owner: sbom-nexus` and the Nexus `sbom-nexus.snapshot.v1` schema.
They also carry `repo_manager_context.mode: local-preview` with explicit false
values for authority, persistence, attempt/success advancement, and snapshot
history creation. See `docs/sbom-nexus-client-contract_v1.md` for the pinned
preview and production-service boundary.
Detection covers `uv.lock`, `requirements.txt`, `package-lock.json`, Detection covers `uv.lock`, `requirements.txt`, `package-lock.json`,
`yarn.lock`, `Cargo.lock`, `go.sum`, `.terraform.lock.hcl`, Ansible requirements `yarn.lock`, `Cargo.lock`, `go.sum`, `.terraform.lock.hcl`, Ansible requirements

View file

@ -0,0 +1,72 @@
# SBOM Nexus client contract v1
Repo Manager consumes SBOM Nexus; it does not implement a second scanner,
snapshot store, freshness model, licence classifier, or catch-up scheduler.
This contract pins the boundary used by `RMGR-WP-0011`.
## Authority modes
| Mode | Surface | Authoritative | Persisted | Advances Nexus state |
| --- | --- | --- | --- | --- |
| Local preview | `rmgr sbom scan|licence-report``sbom-nexus scan` | no | no | no |
| Service operation | configured SBOM Nexus HTTP client | yes | Nexus decides | yes, according to the route outcome |
Every compatibility-command result carries:
```json
{
"repo_manager_context": {
"mode": "local-preview",
"authoritative": false,
"persisted": false,
"advances_last_attempt_at": false,
"advances_last_success_at": false,
"creates_snapshot_history": false
}
}
```
Writing preview JSON to a file creates optional local evidence only. It does
not turn the result into an authoritative snapshot or ingest receipt.
## Pinned snapshot envelope
Repo Manager accepts `schema: sbom-nexus.snapshot.v1`. Additive fields are
compatible. An unknown schema or omission of any of these fields is a contract
error: `schema`, `ok`, `repo_slug`, `source_revision`, `generated_at`,
`entry_count`, `entries`, `sources`, `licence_report`, and `errors`.
Repo Manager may project the existing licence-report compatibility shape from
that envelope. It must not recalculate licence groups or copyleft signals.
## Authoritative service routes
The production client may consume these Nexus-owned routes without depending
on Nexus tables or migrations:
| Purpose | Route |
| --- | --- |
| Upsert repository projection | `PUT /repositories/{repo_slug}` |
| Read repository projections | `GET /repositories/` |
| Persist scan outcome | `POST /sbom/{repo_slug}/ingest` |
| Persist terminal skip outcome | `POST /sbom/{repo_slug}/skip` |
| Read latest repository SBOM | `GET /sbom/{repo_slug}` |
| Read immutable snapshot detail | `GET /sbom/snapshots/{snapshot_id}` |
| Read licence report | `GET /sbom/report/licences/` |
Calls use a bounded timeout and surface deterministic transport, HTTP, and
contract errors. Credentials enter only through the platform runtime path and
must never appear in files, command output, or logs.
## Repository source identity and provenance
Repo Manager remains authoritative for repository slug, active state,
checkout/source identity, and Git revision. An authoritative scan request must
refer to a controlled source artifact or checkout pinned to that revision. It
must not infer identity from a path string or repository name, and it must not
mount a workstation filesystem into the cluster.
`CUST-WP-0064` owns the coordinated source-input topology. Until that topology
is live, local checkout scans remain previews even when their Git revision is
known. SBOM Nexus alone records the resulting authoritative snapshot and
attempt/success history.

View file

@ -347,12 +347,18 @@ def main(argv: list[str] | None = None) -> int:
help="Deprecated compatibility commands delegated to SBOM Nexus", help="Deprecated compatibility commands delegated to SBOM Nexus",
) )
sbom_sub = p_sbom.add_subparsers(dest="sbom_command") sbom_sub = p_sbom.add_subparsers(dest="sbom_command")
p_sbom_scan = sbom_sub.add_parser("scan", help="Scan recognised lockfiles and tool manifests") p_sbom_scan = sbom_sub.add_parser(
"scan",
help="Run a local non-authoritative preview through the SBOM Nexus CLI",
)
p_sbom_scan.add_argument("--path", default=".") p_sbom_scan.add_argument("--path", default=".")
p_sbom_scan.add_argument("--slug", default=None) p_sbom_scan.add_argument("--slug", default=None)
p_sbom_scan.add_argument("--output", default=None, help="Write the derived snapshot as JSON") p_sbom_scan.add_argument("--output", default=None, help="Write the derived snapshot as JSON")
p_sbom_scan.add_argument("--force", action="store_true", help="Replace an existing --output file") p_sbom_scan.add_argument("--force", action="store_true", help="Replace an existing --output file")
p_sbom_report = sbom_sub.add_parser("licence-report", help="Report licences from a fresh file scan") p_sbom_report = sbom_sub.add_parser(
"licence-report",
help="Preview licences locally without persisting or advancing Nexus state",
)
p_sbom_report.add_argument("--path", default=".") p_sbom_report.add_argument("--path", default=".")
p_sbom_report.add_argument("--slug", default=None) p_sbom_report.add_argument("--slug", default=None)

View file

@ -22,6 +22,7 @@ import httpx
from repo_manager.gitops import GitError, commit_paths, push_ff from repo_manager.gitops import GitError, commit_paths, push_ff
from repo_manager.parse.record import iter_record_files, parse_record_file from repo_manager.parse.record import iter_record_files, parse_record_file
from repo_manager.parse.workplan import parse_workplan_file from repo_manager.parse.workplan import parse_workplan_file
from repo_manager.record_identity import scan_record_identities
LOCK_PATH = Path("/tmp/repo-manager-identifier-registrar.lock") LOCK_PATH = Path("/tmp/repo-manager-identifier-registrar.lock")
@ -379,14 +380,27 @@ def registrar_reconcile(
"""Register missing workplan/task UUIDs through one scoped child process.""" """Register missing workplan/task UUIDs through one scoped child process."""
cid = str(uuid.uuid4()) cid = str(uuid.uuid4())
repo = path.expanduser().resolve() repo = path.expanduser().resolve()
identity = scan_record_identities(repo)
before = _missing_identifiers(repo) before = _missing_identifiers(repo)
evidence: dict[str, Any] = { evidence: dict[str, Any] = {
"repo_path": str(repo), "repo_path": str(repo),
"repo_slug": repo.name, "repo_slug": repo.name,
"api_base": api_base.rstrip("/"), "api_base": api_base.rstrip("/"),
"missing_before": before, "missing_before": before,
"record_identity": identity,
} }
if identity["identity_collisions"]:
return RegistrarResult(
"rejected",
evidence,
{
"code": "record_identity_collision",
"message": "same canonical work-record id has conflicting or incomplete UUID assignments",
},
cid,
)
repair_projection_id = None repair_projection_id = None
if repair_workplan and bootstrap_empty_projection: if repair_workplan and bootstrap_empty_projection:
return RegistrarResult( return RegistrarResult(

View file

@ -14,6 +14,11 @@ from repo_manager.index_store import RepoIndex, WorkRecordEntry, _now
from repo_manager.parse.record import iter_record_files, parse_record_file from repo_manager.parse.record import iter_record_files, parse_record_file
from repo_manager.parse.register import iter_register_files, parse_register_file from repo_manager.parse.register import iter_register_files, parse_register_file
from repo_manager.parse.workplan import iter_workplan_files, parse_workplan_file from repo_manager.parse.workplan import iter_workplan_files, parse_workplan_file
from repo_manager.record_identity import scan_record_identities
class RecordIdentityCollisionError(ValueError):
"""Raised when one canonical record id points at conflicting UUIDs."""
def _slug_from_path(repo_root: Path) -> str: def _slug_from_path(repo_root: Path) -> str:
@ -72,7 +77,7 @@ def observe_repository(repo_root: Path, *, slug: str | None = None) -> tuple[dic
}, },
) )
) )
for task in wp.tasks: for task_number, task in enumerate(wp.tasks, start=1):
records.append( records.append(
WorkRecordEntry( WorkRecordEntry(
kind="task", kind="task",
@ -83,15 +88,23 @@ def observe_repository(repo_root: Path, *, slug: str | None = None) -> tuple[dic
uuid=task.state_hub_task_id, uuid=task.state_hub_task_id,
parent_id=wp.id, parent_id=wp.id,
extra={ extra={
key: task.raw[key] "source_occurrence": f"{wp.path}#task-block-{task_number}",
for key in ("depends_on", "needs_human", "intervention_note", "blocking_reason") **{
if key in task.raw key: task.raw[key]
for key in (
"depends_on",
"needs_human",
"intervention_note",
"blocking_reason",
)
if key in task.raw
},
}, },
) )
) )
for path in iter_record_files(repo_root): for path in iter_record_files(repo_root):
for record in parse_record_file(path, repo_root=repo_root): for record_number, record in enumerate(parse_record_file(path, repo_root=repo_root), start=1):
records.append( records.append(
WorkRecordEntry( WorkRecordEntry(
kind=record.kind, kind=record.kind,
@ -100,7 +113,10 @@ def observe_repository(repo_root: Path, *, slug: str | None = None) -> tuple[dic
title=record.title, title=record.title,
source_path=record.source_path, source_path=record.source_path,
uuid=record.uuid, uuid=record.uuid,
extra={"record": record.raw}, extra={
"record": record.raw,
"source_occurrence": f"{record.source_path}#record-block-{record_number}",
},
) )
) )
@ -117,6 +133,41 @@ def observe_repository(repo_root: Path, *, slug: str | None = None) -> tuple[dic
) )
) )
identity = scan_record_identities(repo_root)
if identity["identity_collisions"]:
raise RecordIdentityCollisionError(
f"work-record identity collision: {identity['identity_collisions']}"
)
diagnostic_events: list[dict] = []
for duplicate in identity["duplicate_source_occurrences"]:
matches = [
record
for record in records
if record.kind == duplicate["kind"] and record.id == duplicate["id"]
]
if len(matches) < 2:
continue
keeper = matches[0]
keeper.extra.pop("source_occurrence", None)
keeper.extra["source_occurrences"] = duplicate["sources"]
records = [record for record in records if record is keeper or record not in matches]
diagnostic_events.append(
{
"type": "repo.work_record.duplicate_source_occurrence",
"severity": "warning",
"record_kind": duplicate["kind"],
"record_id": duplicate["id"],
"uuid": duplicate["uuid"],
"source_occurrences": duplicate["sources"],
"cleanup_required": True,
"canon": identity["canon"],
}
)
for record in records:
record.extra.pop("source_occurrence", None)
sha = head_sha(repo_root) if is_git_repo(repo_root) else None sha = head_sha(repo_root) if is_git_repo(repo_root) else None
fingerprint, source_files = source_fingerprint(repo_root) fingerprint, source_files = source_fingerprint(repo_root)
index = RepoIndex( index = RepoIndex(
@ -127,6 +178,7 @@ def observe_repository(repo_root: Path, *, slug: str | None = None) -> tuple[dic
source_fingerprint=fingerprint, source_fingerprint=fingerprint,
source_files=source_files, source_files=source_files,
work_records=records, work_records=records,
events=diagnostic_events,
) )
snapshot = { snapshot = {

View file

@ -71,7 +71,9 @@ def iter_record_files(repo_root: Path) -> list[Path]:
root = repo_root / relative root = repo_root / relative
if root.is_dir(): if root.is_dir():
files.update(path for path in root.rglob("*.md") if not path.name.startswith(".")) files.update(path for path in root.rglob("*.md") if not path.name.startswith("."))
for name in ("INTAKES.md", "DECISIONS.md"): # Fleet repositories use both title-case conventions. Keep these explicit:
# arbitrary top-level Markdown remains outside the governed record surface.
for name in ("INTAKES.md", "DECISIONS.md", "intakes.md", "decisions.md"):
path = repo_root / name path = repo_root / name
if path.is_file(): if path.is_file():
files.add(path) files.add(path)

View file

@ -0,0 +1,91 @@
"""Work-record identity validation and duplicate-source reconciliation."""
from __future__ import annotations
import re
from collections import defaultdict
from pathlib import Path
from typing import Any
import yaml
from repo_manager.parse.record import iter_record_files, parse_record_file
from repo_manager.parse.workplan import iter_workplan_files, parse_workplan_file
DEFAULT_KIND_REGISTRY = Path(__file__).resolve().parents[2] / "config" / "work-record-types.yaml"
CANON_WORK_RECORD_TYPES = "the-custodian/canon/standards/work-record-types.yaml"
def load_kind_registry(path: Path | None = None) -> dict[str, Any]:
data = yaml.safe_load((path or DEFAULT_KIND_REGISTRY).read_text(encoding="utf-8")) or {}
if not isinstance(data, dict) or not isinstance(data.get("kinds"), list):
raise TypeError("invalid work-record kind registry")
return data
def classify_record_id(
kind: str, identifier: str, *, registry_path: Path | None = None
) -> str | None:
"""Return canonical/grandfathered when the canon registry accepts an id."""
registry = load_kind_registry(registry_path)
entry = next((item for item in registry["kinds"] if item.get("kind") == kind), None)
if not isinstance(entry, dict):
return None
if any(re.fullmatch(pattern, identifier) for pattern in entry.get("id_patterns") or []):
return "canonical"
for legacy in entry.get("legacy_patterns") or []:
if legacy.get("grandfathered") and re.fullmatch(
str(legacy.get("pattern") or ""), identifier
):
return "grandfathered"
return None
def scan_record_identities(repo_root: Path, *, registry_path: Path | None = None) -> dict[str, Any]:
"""Describe invalid ids, benign duplicate sources, and hard collisions."""
occurrences: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list)
invalid: list[dict[str, str]] = []
def add(kind: str, identifier: str | None, record_uuid: str | None, source: str) -> None:
if not identifier:
return
classification = classify_record_id(kind, identifier, registry_path=registry_path)
item = {"kind": kind, "id": identifier, "uuid": record_uuid, "source": source}
occurrences[(kind, identifier)].append(item)
if classification is None:
invalid.append({"kind": kind, "id": identifier, "source": source})
for path in iter_workplan_files(repo_root):
workplan = parse_workplan_file(path, repo_root=repo_root)
add("workplan", workplan.id, workplan.state_hub_workstream_id, workplan.path)
for number, task in enumerate(workplan.tasks, start=1):
add("task", task.id, task.state_hub_task_id, f"{workplan.path}#task-block-{number}")
for path in iter_record_files(repo_root):
for number, record in enumerate(parse_record_file(path, repo_root=repo_root), start=1):
add(record.kind, record.id, record.uuid, f"{record.source_path}#record-block-{number}")
duplicates: list[dict[str, Any]] = []
collisions: list[dict[str, Any]] = []
for (kind, identifier), items in sorted(occurrences.items()):
if len(items) < 2:
continue
uuids = {item["uuid"] for item in items}
result = {
"kind": kind,
"id": identifier,
"uuid": items[0]["uuid"] if len(uuids) == 1 else None,
"sources": [item["source"] for item in items],
}
if len(uuids) == 1 and None not in uuids:
duplicates.append(result)
else:
result["uuids"] = sorted(str(value) for value in uuids)
collisions.append(result)
return {
"ok": not invalid and not collisions,
"invalid_identifiers": invalid,
"duplicate_source_occurrences": duplicates,
"identity_collisions": collisions,
"canon": CANON_WORK_RECORD_TYPES,
}

View file

@ -1,4 +1,5 @@
"""Deprecated Repo Manager CLI adapter for the SBOM Nexus product owner.""" """Deprecated Repo Manager CLI adapter for the SBOM Nexus product owner."""
from __future__ import annotations from __future__ import annotations
import json import json
@ -8,9 +9,55 @@ import subprocess
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
SNAPSHOT_SCHEMA = "sbom-nexus.snapshot.v1"
SNAPSHOT_REQUIRED_FIELDS = frozenset(
{
"schema",
"ok",
"repo_slug",
"source_revision",
"generated_at",
"entry_count",
"entries",
"sources",
"licence_report",
"errors",
}
)
PREVIEW_CONTEXT = {
"mode": "local-preview",
"authoritative": False,
"persisted": False,
"advances_last_attempt_at": False,
"advances_last_success_at": False,
"creates_snapshot_history": False,
}
class SBOMContractError(ValueError):
"""Raised when a Nexus response is outside the pinned consumer contract."""
def validate_snapshot_contract(payload: dict[str, Any]) -> None:
"""Accept additive fields but reject unknown schemas or missing required fields."""
if payload.get("schema") != SNAPSHOT_SCHEMA:
raise SBOMContractError(
f"unsupported SBOM Nexus schema {payload.get('schema')!r}; expected {SNAPSHOT_SCHEMA!r}"
)
missing = sorted(SNAPSHOT_REQUIRED_FIELDS - payload.keys())
if missing:
raise SBOMContractError(f"SBOM Nexus snapshot is missing required fields: {missing}")
def _mark_preview(payload: dict[str, Any]) -> dict[str, Any]:
payload["repo_manager_context"] = dict(PREVIEW_CONTEXT)
payload["delegated_by"] = "repo-manager"
payload["product_owner"] = "sbom-nexus"
return payload
def scan_repository_via_nexus(repo_root: Path, *, slug: str | None = None) -> dict[str, Any]: def scan_repository_via_nexus(repo_root: Path, *, slug: str | None = None) -> dict[str, Any]:
"""Delegate a local source scan to SBOM Nexus without owning scanner logic.""" """Delegate a non-authoritative local preview to the Nexus-owned CLI."""
executable = os.getenv("SBOM_NEXUS_CLI") or shutil.which("sbom-nexus") executable = os.getenv("SBOM_NEXUS_CLI") or shutil.which("sbom-nexus")
if not executable: if not executable:
return _error( return _error(
@ -41,8 +88,12 @@ def scan_repository_via_nexus(repo_root: Path, *, slug: str | None = None) -> di
if not isinstance(payload, dict): if not isinstance(payload, dict):
return _error("SBOM Nexus CLI returned a non-object JSON response") return _error("SBOM Nexus CLI returned a non-object JSON response")
payload["delegated_by"] = "repo-manager" try:
payload["product_owner"] = "sbom-nexus" validate_snapshot_contract(payload)
except SBOMContractError as exc:
return _error(str(exc), reason="sbom-nexus-contract")
_mark_preview(payload)
if completed.returncode and payload.get("ok", True): if completed.returncode and payload.get("ok", True):
payload["ok"] = False payload["ok"] = False
payload.setdefault("errors", []).append( payload.setdefault("errors", []).append(
@ -62,25 +113,27 @@ def licence_report_from_snapshot(snapshot: dict[str, Any]) -> dict[str, Any]:
"source_revision": snapshot.get("source_revision"), "source_revision": snapshot.get("source_revision"),
"generated_at": snapshot.get("generated_at"), "generated_at": snapshot.get("generated_at"),
"entry_count": int(snapshot.get("entry_count") or 0), "entry_count": int(snapshot.get("entry_count") or 0),
"licence_report": snapshot.get("licence_report") or { "licence_report": snapshot.get("licence_report")
or {
"groups": [], "groups": [],
"copyleft_direct_prod": [], "copyleft_direct_prod": [],
"copyleft_direct_count": 0, "copyleft_direct_count": 0,
}, },
"errors": snapshot.get("errors") or [], "errors": snapshot.get("errors") or [],
"repo_manager_context": dict(PREVIEW_CONTEXT),
"delegated_by": "repo-manager", "delegated_by": "repo-manager",
"product_owner": "sbom-nexus", "product_owner": "sbom-nexus",
} }
def _error(detail: str) -> dict[str, Any]: def _error(detail: str, *, reason: str = "sbom-nexus-delegation") -> dict[str, Any]:
return { return _mark_preview(
"schema": "sbom-nexus.snapshot.v1", {
"ok": False, "schema": SNAPSHOT_SCHEMA,
"entry_count": 0, "ok": False,
"entries": [], "entry_count": 0,
"sources": [], "entries": [],
"errors": [{"reason": "sbom-nexus-delegation", "detail": detail}], "sources": [],
"delegated_by": "repo-manager", "errors": [{"reason": reason, "detail": detail}],
"product_owner": "sbom-nexus", }
} )

View file

@ -18,6 +18,7 @@ from typing import Any, Literal
from repo_manager.observe import load_classification from repo_manager.observe import load_classification
from repo_manager.parse.workplan import parse_frontmatter from repo_manager.parse.workplan import parse_frontmatter
from repo_manager.record_identity import scan_record_identities
CANON_FLAVOR = "the-custodian/canon/standards/project-repository-flavor_v0.1.md" CANON_FLAVOR = "the-custodian/canon/standards/project-repository-flavor_v0.1.md"
CANON_CLASSIFICATION = "the-custodian/canon/standards/repo-classification-standard_v1.0.md" CANON_CLASSIFICATION = "the-custodian/canon/standards/repo-classification-standard_v1.0.md"
@ -274,4 +275,41 @@ def check_repository(repo_root: Path, *, slug: str | None = None) -> Conformance
canon="ADR-007", canon="ADR-007",
) )
) )
identity = scan_record_identities(repo_root)
for item in identity["invalid_identifiers"]:
report.findings.append(
Finding(
code="work-record-id-invalid",
severity="contradictory",
path=item["source"],
message=f"{item['kind']} id {item['id']!r} is not accepted by the canon kind registry",
canon=identity["canon"],
)
)
for item in identity["duplicate_source_occurrences"]:
report.findings.append(
Finding(
code="work-record-duplicate-source-occurrence",
severity="warning",
path=item["sources"][0],
message=(
f"{item['kind']} {item['id']!r} repeats with UUID {item['uuid']}; "
f"index once and clean up sources {item['sources']}"
),
canon=identity["canon"],
)
)
for item in identity["identity_collisions"]:
report.findings.append(
Finding(
code="work-record-identity-collision",
severity="contradictory",
path=item["sources"][0],
message=(
f"{item['kind']} {item['id']!r} has conflicting UUID assignments "
f"at {item['sources']}"
),
canon=identity["canon"],
)
)
return report return report

View file

@ -0,0 +1,87 @@
from pathlib import Path
import pytest
from repo_manager.observe import RecordIdentityCollisionError, observe_repository
from repo_manager.record_identity import classify_record_id, scan_record_identities
from repo_manager.standards import check_repository
def _write_mason_workplan(repo: Path, second_uuid: str) -> None:
path = repo / "workplans" / "MASON-0001-statehub-bootstrap.md"
path.parent.mkdir(parents=True)
path.write_text(
f"""---
id: MASON-0001
type: workplan
title: Bootstrap
status: finished
state_hub_workstream_id: "11111111-1111-4111-8111-111111111111"
---
## First occurrence
```task
id: MASON-0001-T01
status: done
priority: high
state_hub_task_id: "ede1aa62-fc42-494a-9984-3190a52481af"
```
## Second occurrence
```task
id: MASON-0001-T01
status: done
priority: high
state_hub_task_id: "{second_uuid}"
```
""",
encoding="utf-8",
)
def test_grandfathered_mason_identity_is_loaded_from_canon_registry() -> None:
assert classify_record_id("workplan", "MASON-0001") == "grandfathered"
assert classify_record_id("task", "MASON-0001-T01") == "grandfathered"
def test_same_id_and_uuid_indexes_once_with_cleanup_diagnostic(tmp_path: Path) -> None:
_write_mason_workplan(tmp_path, "ede1aa62-fc42-494a-9984-3190a52481af")
report = scan_record_identities(tmp_path)
assert report["identity_collisions"] == []
assert report["duplicate_source_occurrences"][0]["sources"] == [
"workplans/MASON-0001-statehub-bootstrap.md#task-block-1",
"workplans/MASON-0001-statehub-bootstrap.md#task-block-2",
]
snapshot, index = observe_repository(tmp_path, slug="ops-mason")
tasks = [record for record in index.work_records if record.kind == "task"]
assert snapshot["index"]["task_count"] == 1
assert len(tasks) == 1
assert (
tasks[0].extra["source_occurrences"] == report["duplicate_source_occurrences"][0]["sources"]
)
assert index.events[0]["type"] == "repo.work_record.duplicate_source_occurrence"
conformance = check_repository(tmp_path, slug="ops-mason")
codes = [finding.code for finding in conformance.findings]
assert "work-record-duplicate-source-occurrence" in codes
assert not any(
finding.code == "work-record-id-invalid" and finding.path.startswith("workplans/")
for finding in conformance.findings
)
def test_same_id_with_different_uuids_fails_closed(tmp_path: Path) -> None:
_write_mason_workplan(tmp_path, "22222222-2222-4222-8222-222222222222")
report = scan_record_identities(tmp_path)
assert report["duplicate_source_occurrences"] == []
assert report["identity_collisions"][0]["id"] == "MASON-0001-T01"
with pytest.raises(RecordIdentityCollisionError):
observe_repository(tmp_path, slug="ops-mason")
conformance = check_repository(tmp_path, slug="ops-mason")
assert any(finding.code == "work-record-identity-collision" for finding in conformance.findings)

View file

@ -120,6 +120,39 @@ status: open
assert missing["decisions"] == ["DEMO-DEC-0001"] assert missing["decisions"] == ["DEMO-DEC-0001"]
def test_missing_scan_includes_lowercase_top_level_record_files(tmp_path: Path) -> None:
repo = _fixture(tmp_path)
(repo / "intakes.md").write_text(
"""# Intakes
```yaml
id: DEMO-IN-0002
kind: intake
title: Standalone intake
status: open
```
""",
encoding="utf-8",
)
(repo / "decisions.md").write_text(
"""# Decisions
```yaml
id: DEMO-DEC-0002
kind: decision
title: Standalone decision
status: accepted
```
""",
encoding="utf-8",
)
missing = rr._missing_identifiers(repo)
assert missing["intakes"] == ["DEMO-IN-0002"]
assert missing["decisions"] == ["DEMO-DEC-0002"]
def test_scopes_registrar_env_and_commits_assigned_ids(tmp_path: Path, monkeypatch) -> None: def test_scopes_registrar_env_and_commits_assigned_ids(tmp_path: Path, monkeypatch) -> None:
repo = _fixture(tmp_path) repo = _fixture(tmp_path)
monkeypatch.setattr(rr, "_check_primary", lambda _api: ({"status": "ok", "db": "connected"}, None)) monkeypatch.setattr(rr, "_check_primary", lambda _api: ({"status": "ok", "db": "connected"}, None))

View file

@ -4,10 +4,14 @@ import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
import pytest
from repo_manager.cli import main from repo_manager.cli import main
from repo_manager.sbom_client import ( from repo_manager.sbom_client import (
SBOMContractError,
licence_report_from_snapshot, licence_report_from_snapshot,
scan_repository_via_nexus, scan_repository_via_nexus,
validate_snapshot_contract,
) )
@ -74,6 +78,14 @@ def test_scan_delegates_to_sbom_nexus_without_shell(monkeypatch, tmp_path: Path)
assert result["schema"] == "sbom-nexus.snapshot.v1" assert result["schema"] == "sbom-nexus.snapshot.v1"
assert result["product_owner"] == "sbom-nexus" assert result["product_owner"] == "sbom-nexus"
assert result["delegated_by"] == "repo-manager" assert result["delegated_by"] == "repo-manager"
assert result["repo_manager_context"] == {
"mode": "local-preview",
"authoritative": False,
"persisted": False,
"advances_last_attempt_at": False,
"advances_last_success_at": False,
"creates_snapshot_history": False,
}
def test_missing_nexus_cli_returns_actionable_error(monkeypatch, tmp_path: Path) -> None: def test_missing_nexus_cli_returns_actionable_error(monkeypatch, tmp_path: Path) -> None:
@ -98,10 +110,41 @@ def test_licence_report_alias_preserves_shape() -> None:
"entry_count", "entry_count",
"licence_report", "licence_report",
"errors", "errors",
"repo_manager_context",
"delegated_by", "delegated_by",
"product_owner", "product_owner",
} }
assert result["licence_report"]["copyleft_direct_count"] == 0 assert result["licence_report"]["copyleft_direct_count"] == 0
assert result["repo_manager_context"]["authoritative"] is False
def test_snapshot_contract_allows_additive_fields_and_rejects_unknown_schema() -> None:
validate_snapshot_contract({**_snapshot(), "future_addition": {"accepted": True}})
with pytest.raises(SBOMContractError, match="unsupported SBOM Nexus schema"):
validate_snapshot_contract({**_snapshot(), "schema": "sbom-nexus.snapshot.v2"})
def test_scan_turns_unknown_schema_into_deterministic_contract_error(
monkeypatch, tmp_path: Path
) -> None:
monkeypatch.setenv("SBOM_NEXUS_CLI", "/opt/sbom-nexus/bin/sbom-nexus")
monkeypatch.setattr(
subprocess,
"run",
lambda command, **kwargs: subprocess.CompletedProcess(
command,
0,
json.dumps({**_snapshot(), "schema": "future.snapshot.v9"}),
"",
),
)
result = scan_repository_via_nexus(tmp_path)
assert result["ok"] is False
assert result["errors"][0]["reason"] == "sbom-nexus-contract"
assert result["repo_manager_context"]["persisted"] is False
def test_cli_scan_preserves_output_file_behavior(monkeypatch, tmp_path: Path, capsys) -> None: def test_cli_scan_preserves_output_file_behavior(monkeypatch, tmp_path: Path, capsys) -> None:

View file

@ -288,6 +288,25 @@ two older workplans still carry production-absent random UUIDs and remain T04
work; T02 therefore stays `wait`. Evidence: work; T02 therefore stays `wait`. Evidence:
`docs/evidence/RMGR-WP-0005-rail-kubernetes-registrar-2026-08-22.md`. `docs/evidence/RMGR-WP-0005-rail-kubernetes-registrar-2026-08-22.md`.
**Standalone-record and duplicate-source hardening (2026-08-22):** owner
feedback exposed that the governed top-level `intakes.md`/`decisions.md`
filenames were absent from Repo Manager's explicit record-file allowlist.
Consequently an intake-only reconciliation could return `noop`; it happened to
work only when another missing record caused State Hub's broader pass to run.
The scanner now recognizes both lowercase and uppercase governed filenames,
and a direct scan of the Custodian source returns exactly `CUST-IN-0014` as
missing.
The registrar and conformance paths now also implement the Custodian kind
registry's identity-reconciliation ruling. Repeated canonical id plus the same
non-null UUID is one indexed record with all source occurrences retained and a
governed cleanup warning; conflicting or incomplete UUID assignments fail
closed. The grandfathered `MASON-0001` and `MASON-0001-TNN` forms are loaded
from the machine-readable canon mapping instead of being rewritten. Live proof
against `ops-mason` indexes `MASON-0001-T01` once, preserves both task-block
locations, and emits the cleanup diagnostic. T02 remains `wait` because its
original pre-derivation reconciliation inventory is not yet exhausted.
## Derive identifiers deterministically ## Derive identifiers deterministically
```task ```task

View file

@ -4,7 +4,7 @@ type: workplan
title: "SBOM Nexus production client and explicit preview semantics" title: "SBOM Nexus production client and explicit preview semantics"
domain: infotech domain: infotech
repo: repo-manager repo: repo-manager
status: ready status: active
owner: codex owner: codex
topic_slug: infotech topic_slug: infotech
quality_dor: DoR-Ok quality_dor: DoR-Ok
@ -13,6 +13,7 @@ updated: "2026-08-22"
parent_workplan: SBOM-WP-0002 parent_workplan: SBOM-WP-0002
related: related:
- CUST-WP-0062 - CUST-WP-0062
- CUST-WP-0064
- RMGR-WP-0008 - RMGR-WP-0008
state_hub_workstream_id: "a6cd9248-e591-51fc-82b0-a0f3799c3939" state_hub_workstream_id: "a6cd9248-e591-51fc-82b0-a0f3799c3939"
--- ---
@ -36,7 +37,7 @@ for callers that require authoritative state.
```task ```task
id: RMGR-WP-0011-T01 id: RMGR-WP-0011-T01
status: todo status: done
priority: high priority: high
state_hub_task_id: "b3b0f4d9-de14-5429-bc4d-a14d014491b0" state_hub_task_id: "b3b0f4d9-de14-5429-bc4d-a14d014491b0"
``` ```
@ -46,6 +47,15 @@ authoritative service routes. Treat additive fields as compatible, reject an
unknown schema, and do not couple Repo Manager to Nexus database tables or unknown schema, and do not couple Repo Manager to Nexus database tables or
migration internals. migration internals.
**Result (2026-08-22):** `docs/sbom-nexus-client-contract_v1.md` pins the
accepted `sbom-nexus.snapshot.v1` envelope and the Nexus-owned repository,
ingest/skip, latest-snapshot, immutable-history, and licence-report routes.
Additive response fields remain compatible; unknown schemas and missing
required fields produce deterministic contract errors. The source handoff now
joins `CUST-WP-0064`: Repo Manager supplies repository identity and revision,
while the authoritative scan consumes a controlled revision-pinned source. A
workstation filesystem must never be mounted into the cluster.
## Add the authoritative Nexus service client ## Add the authoritative Nexus service client
```task ```task
@ -64,7 +74,7 @@ introduced through the platform path, must never enter files, output, or logs.
```task ```task
id: RMGR-WP-0011-T03 id: RMGR-WP-0011-T03
status: todo status: done
priority: high priority: high
state_hub_task_id: "5cd717de-d696-5676-9abe-f1a701e48e7e" state_hub_task_id: "5cd717de-d696-5676-9abe-f1a701e48e7e"
``` ```
@ -75,6 +85,13 @@ non-authoritative, and not persisted. The compatibility aliases must not imply
that a preview advanced `last_attempt_at`, `last_success_at`, or snapshot that a preview advanced `last_attempt_at`, `last_success_at`, or snapshot
history. history.
**Result (2026-08-22):** both compatibility aliases now identify every success
and error as `mode: local-preview`, `authoritative: false`, `persisted: false`,
and explicitly state that no last-attempt, last-success, or snapshot-history
state advances. Saving preview JSON remains optional local evidence and cannot
be interpreted as an ingest receipt. CLI help, operator documentation, and
contract tests carry the same semantics.
## Prove cutover and remove ambiguity ## Prove cutover and remove ambiguity
```task ```task