Files
boshu2__agentops/scripts/check-bounded-contexts-drift.sh
T
Bo f55cbb3dcd chore(scripts): add registry drift detector for DDD/hex registry docs (soc-ry4a) (#308)
## What

Adds \`scripts/check-registry-drift.sh\` — a drift detector for the
hand-maintained DDD/hex registry docs. Detects three classes of drift:

1. **Count drift** — narrative + audit-table + Gherkin scenario counts
vs actual \`skills/\` directory count
2. **Membership drift** — every skill listed in the Full Skill Map
exists in \`skills/\` and vice versa
3. **Role drift** — \`hexagonal_role\` column in the map matches each
\`SKILL.md\` frontmatter

## Why

Encodes the \`registries-drift\` lesson from session 2026-05-17:

> Hand-maintained inventory docs drift silently. Three axis owners (DDD
vocab, Hex frontmatter, Gherkin features) must be generated or
schema-gated; never hand-edited maps.

Pre-PR state (detected by the new script):

| Doc | Declared | Actual |
|---|---|---|
| \`agentops-skill-domain-map.md\` narrative | 77 skills | 78 |
| \`agentops-skill-domain-map.md\` audit table | 78 | 78 ✓ |
| \`agentops-domain-evolution-bdd.md\` Gherkin | 77 skills | 78 |
| \`skills/expert-council/SKILL.md\` | no \`hexagonal_role\` | declared
\`domain\` in map |

All four reconciled in this PR; final check is PASS.

## Implementation

### \`scripts/check-registry-drift.sh\`

- Modes: \`--check\` (default), \`--fix-counts\` (auto-update inline
narrative counts), \`--json\` (machine-readable report)
- Returns exit 1 on FAIL, 0 on PASS/WARN
- Runtime: < 1s
- Dep: PyYAML (already required for context-map generator)

### Wired into:

- \`scripts/pre-push-gate.sh\` — always-runs section (drift can be
introduced by any skill add/rename/remove)
- \`.github/workflows/validate.yml\` — new \`validate-registry-drift\`
job + added to \`summary\` job's \`needs\` (so it gates merges)

### Reconciliation

- 77 → 78 in 2 doc locations (via \`--fix-counts\`)
- \`hexagonal_role: domain\` added to expert-council frontmatter
(consistent with council, which it aliases to)
- documentation-index.md count updated + annotation added

### Codex parity

\`skills-codex/expert-council/.agentops-generated.json\` source_hash
auto-updated. \`generated_hash\` unchanged → codex artifact is
byte-identical, no regen required. \`bash
scripts/check-codex-parity-drift.sh\` returns PASS.

## Acceptance

- [x] \`scripts/check-registry-drift.sh\` exists, runs in < 1s
- [x] Detects count/membership/role drift; current state PASSES after
reconciliation
- [x] Wired into pre-push-gate.sh — confirmed gate runs (\`registry
drift: ok\`)
- [x] Wired into validate.yml summary needs list
- [x] Linked from documentation-index.md
- [x] Lands via PR (governed by branch protection — dogfoods discipline)

Closes-scenario: drift-detector-encoded
Bounded-context: scripts/ + CI gates
Evidence: scripts/check-registry-drift.sh, validate-registry-drift job,
PASS verdict
Closes: soc-ry4a

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-18 14:19:16 +00:00

163 lines
5.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# scripts/check-bounded-contexts-drift.sh
#
# Verify the BC1-BC5 definitions in docs/contracts/bounded-contexts.yaml
# (canonical) match the prose used in the registry docs that classify
# skills against them.
#
# Encodes Phase 2 of the registries-drift remediation (soc-zxia.2):
# extract BC1-BC5 definitions to a single yaml source-of-truth so that
# the same five concepts cannot be restated with drift in 3 places.
#
# Checks:
# 1. Every BC id+name pair in the yaml appears verbatim as a row prefix
# in docs/reference/agentops-skill-domain-map.md Domain Taxonomy table.
# 2. Every BC id+name pair appears in docs/reference/agentops-hexagonal-
# architecture-map.md Bounded Contexts table.
# 3. Each BC's responsibility (canonical sentence) appears verbatim in
# both of the above docs.
# 4. Each BC's product_layer string appears in skill-domain-map.md.
# 5. Each BC's port names appear in hexagonal-architecture-map.md.
#
# Exit codes:
# 0 = no drift
# 1 = drift detected
# 2 = usage / missing input
#
# Modes:
# --check (default) report drift
# --json machine-readable report
#
# Lesson: .agents/learnings/2026-05-17-registries-drift.md
# Phase: soc-zxia.2 (after soc-zxia.1 schema-gate, before soc-zxia.3 generators)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
BC_YAML="${REPO_ROOT}/docs/contracts/bounded-contexts.yaml"
MAP_DOC="${REPO_ROOT}/docs/reference/agentops-skill-domain-map.md"
HEX_DOC="${REPO_ROOT}/docs/reference/agentops-hexagonal-architecture-map.md"
JSON_OUT=0
for arg in "$@"; do
case "$arg" in
--check) ;;
--json) JSON_OUT=1 ;;
-h|--help)
sed -n '2,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
;;
*)
echo "ERROR: unknown arg: $arg (try --help)" >&2
exit 2
;;
esac
done
for f in "${BC_YAML}" "${MAP_DOC}" "${HEX_DOC}"; do
if [[ ! -f "$f" ]]; then
echo "ERROR: required file missing: $f" >&2
exit 2
fi
done
export BC_YAML MAP_DOC HEX_DOC JSON_OUT
exec python3 - <<'PY'
import json
import os
import sys
from pathlib import Path
try:
import yaml
except ImportError:
print("ERROR: PyYAML not installed; install with: pip install pyyaml", file=sys.stderr)
sys.exit(2)
BC_YAML = Path(os.environ["BC_YAML"])
MAP_DOC = Path(os.environ["MAP_DOC"])
HEX_DOC = Path(os.environ["HEX_DOC"])
JSON_OUT = os.environ.get("JSON_OUT") == "1"
data = yaml.safe_load(BC_YAML.read_text())
bcs = data.get("bounded_contexts", [])
if len(bcs) != 5:
print(f"ERROR: expected 5 bounded contexts in {BC_YAML.name}, got {len(bcs)}", file=sys.stderr)
sys.exit(2)
map_text = MAP_DOC.read_text()
hex_text = HEX_DOC.read_text()
findings = []
def add(severity, code, msg):
findings.append({"severity": severity, "code": code, "msg": msg})
for bc in bcs:
bc_id = bc["id"]
bc_name = bc["name"]
title = f"{bc_id} {bc_name}" # e.g. "BC1 Corpus"
# Check 1: title appears in map doc
if title not in map_text:
add("fail", "BC_TITLE_MISSING_FROM_MAP",
f"`{title}` not found in {MAP_DOC.name} — every BC must appear in skill-domain-map")
# Check 2: title in hex doc
if title not in hex_text:
add("fail", "BC_TITLE_MISSING_FROM_HEX",
f"`{title}` not found in {HEX_DOC.name} — every BC must appear in hexagonal-architecture-map")
# Check 3: responsibility in both
resp = bc["responsibility"]
if resp not in map_text:
add("fail", "BC_RESP_DRIFT_MAP",
f"`{title}` responsibility in {MAP_DOC.name} drifts from yaml canonical: \"{resp}\"")
if resp not in hex_text:
add("fail", "BC_RESP_DRIFT_HEX",
f"`{title}` responsibility in {HEX_DOC.name} drifts from yaml canonical: \"{resp}\"")
# Check 4: product_layer in map doc
pl = bc.get("product_layer", "")
if pl and pl not in map_text:
add("fail", "BC_PRODUCT_LAYER_DRIFT",
f"`{title}` product_layer in {MAP_DOC.name} drifts from yaml canonical: \"{pl}\"")
# Check 5: ports in hex doc
for port in bc.get("ports", []):
if port not in hex_text:
add("warn", "BC_PORT_MISSING_FROM_HEX",
f"`{title}` port `{port}` declared in yaml but not mentioned in {HEX_DOC.name}")
fails = [f for f in findings if f["severity"] == "fail"]
warns = [f for f in findings if f["severity"] == "warn"]
if JSON_OUT:
print(json.dumps({
"bounded_contexts_checked": len(bcs),
"findings": findings,
"verdict": "FAIL" if fails else ("WARN" if warns else "PASS"),
}, indent=2))
else:
print(f"Bounded-context drift check: {len(bcs)} BCs in {BC_YAML.name}")
print(f" cross-checked against {MAP_DOC.name} + {HEX_DOC.name}")
print()
for f in findings:
tag = {"fail": "FAIL", "warn": "WARN"}[f["severity"]]
print(f"[{tag}] {f['code']}: {f['msg']}")
print()
if not findings:
print("PASS — registry docs match yaml canonical.")
elif fails:
print(f"FAIL — {len(fails)} drift finding(s), {len(warns)} warning(s)")
else:
print(f"WARN — {len(warns)} warning(s)")
sys.exit(1 if fails else 0)
PY