fix(docs,ci): the service context file described the template repo, and three guards were blind - #89
Merged
Conversation
…nd three guards were blind Closing the three items left open after #88. Each turned out to sit on top of a guard that could not see the thing it guarded. 1. templates/service/CLAUDE.md ships into every scaffolded service and still described the pre-migration template repo: a templates/ tree with cicd/, monitoring/ and common_utils/ that ADR-030 dissolved in June, commands telling the adopter to scaffold a service from inside their service, and "18 rules + 26 skills + 18 workflows" against a live 19/27/20. Rewritten to the service's own layout and command set, with the template-repo audit history replaced by an actionable Upstream template section. C4 now reconciles BOTH CLAUDE.md files. The service copy was only ever checked inside a generated service — by which point it had shipped wrong. 2. The service Makefile ran `copier update --trust --defaults` with no --vcs-ref. /scaffold-update was pinned in #71; the Makefile target, the other entry point to the same operation, was not. Worse, it reused REF, which ci-green defines as `REF ?= main`, so a bare `make scaffold-update` updated the service from the moving development branch rather than a release. Now TEMPLATE_REF with no default: it refuses to run unpinned, exit 2, without invoking copier. check_adopter_scaffold_ref.py could not see this: its scan was keyed on SCAN_EXTENSIONS and a Makefile has no extension — while its own comment warned that "a guard whose coverage is a literal list is only ever as complete as the moment someone last remembered to edit it". Scoping by extension was the same mistake. Extensionless build files are now scanned, and the guard's message no longer describes the v1.x downgrade catastrophe that ADR-045 already removed. 3. Three sys.path bridges in templates/service/tests/ probed directories where common_utils has not lived since ADR-030 — dead safety nets that could never fire, masked because the import resolves by another route in the contexts CI exercises. All now use parents[1], the service root in both layouts. test_drills_reproducible.py's unreachable DRILL_PYTHONPATH fallback became an assertion that would notice a future divergence. test_memory_contracts.py resolved the service root as REPO_ROOT/"templates"/"service", which does not exist inside an adopter's service, so the invariant "serving code must not import common_utils.memory_types" silently SKIPPED in exactly the environment it protects. Now context-adaptive, and verified to fail on a planted import. The path gate is extended from .md/.txt to code comments. Measured before widening: 15 unresolved paths across 309 code files, including .security-baselines/tfsec.yml justifying three suppressed HIGH findings against a deleted directory. All 15 resolved. Three filters the first run demanded are each pinned by a test: glob and brace shorthands no longer report a truncated prefix, uppercase stand-ins and *.local.* paths are not claims, and punctuation stripping no longer scrubs an ellipsis into a clean-looking path. String literals stay out of scope — a path built at runtime is program logic, not a claim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the three items left open after #88. Each sat on top of a guard that could not see the thing it was guarding.
1. The service context file described the template repo
templates/service/CLAUDE.mdships into every scaffolded service and still described the pre-migration template repo:templates/tree withcicd/,monitoring/andcommon_utils/— dissolved by ADR-030 in June;bash templates/scripts/new-service.sh— telling the adopter to scaffold a service from inside their service;examples/minimal,releases/,CHANGELOG.md,.cursor/rules/— none of which exist in a generated service;Rewritten to the service's own layout and command set (
make train,make drift-check,make audit-rules…), with the template-repo audit history replaced by an actionable Upstream template section.Why nothing caught it: the path gate reads backticked spans, and this is an ASCII tree. Doc-coherence C4 does check surface counts — but only the root
CLAUDE.md. The service copy is reconciled by the same script inside a generated service, i.e. after it has already shipped wrong. C4 now reconciles both, each against the agentic surface beside it.2.
make scaffold-updaterancopier updateunpinned — defaulting tomainThe service
Makefileran:/scaffold-updatewas pinned in #71. The Makefile target — the other entry point to the same operation — was not.Worse: it reused
REF, which theci-greentarget defines asREF ?= main. So a baremake scaffold-updateupdated the service from the moving development branch, not a release. Two targets sharing one variable with two different meanings (a git branch vs a template release tag).Now
TEMPLATE_REF, with no default:Why nothing caught it:
check_adopter_scaffold_ref.pyscans bySCAN_EXTENSIONS, and aMakefilehas no extension. Its own comment warns:Scoping by extension was the same mistake wearing a different hat. Extensionless build files are now scanned; the widened guard caught the unpinned target on its first run.
The guard's failure message also described a catastrophe ADR-045 already removed (the 627→435-file v1.x downgrade). Corrected to the current reality: unpinned means jumping to a release nobody chose, and the catastrophic form is prevented by the tag namespace staying clean rather than by the command.
3. Four broken sys.path bridges, and a contract test that self-disabled
Three bridges in
templates/service/tests/probed directories wherecommon_utilshas not lived since ADR-030:common_utilsthere?conftest.pyparents[2]→templates/test_memory_redaction.pyparents[3]+"templates"test_memory_contracts.pyparents[3]+"templates"Dead safety nets — masked because the import resolves by another route in the contexts CI exercises.
conftest.py's comment explains it exists to prevent "52 errors that no CI lane saw"; it has been unable to fire since June. All now useparents[1], which is the service root in both layouts:templates/service/here, the repo root in a scaffolded service.test_drills_reproducible.pycarried an unreachableDRILL_PYTHONPATHfallback for a layout split the migration closed. Replaced by an assertion that would notice if the layouts ever diverge again, rather than a fallback that quietly stops protecting anything.The sharpest one:
test_memory_contracts.pyresolved the service root asREPO_ROOT/"templates"/"service"and skipped if absent. Inside an adopter's service that path does not exist — so the invariant "serving/training code must not importcommon_utils.memory_types" silently skipped in exactly the environment it exists to protect. Now context-adaptive, and verified to fail on a planted import rather than passing vacuously.The path gate now reads code comments
Extended from
.md/.txtto tracked.py,.yml,.yaml,.shandMakefilecomments. I measured before deciding: 15 unresolved paths across 309 code files — small enough for a hard gate, unlike a naive scan of every string literal. Among them,.security-baselines/tfsec.ymljustifying three suppressed HIGH findings against a directory that no longer existed. All 15 resolved.Three filters the first run demanded, each pinned by a test:
deploy-*.yml,deploy-{gcp,aws}.yml.../deploy-gets reportedADR-XXX.md,*.local.*templates/templates/...String literals stay out of scope: a path built at runtime is program logic, not a claim.
Evidence — Schema / Contract Test
templates/tests/unit/test_doc_path_refs_contract.py— extended to 44 cases covering the code-comment scan and all three new filters.Evidence — Real Execution Output
Evidence — CI Run Link
See the checks on this PR.
🤖 Generated with Claude Code