Skip to content

Commit 60eef57

Browse files
docs: compound — durable lessons from v1 upstream bug sweep (#77)
## Summary Compound phase from session-6c091d (PR #76). Four new durable lessons extracted from the v1 upstream bug sweep, plus a clarification of the existing leakage lesson's sweep scope. ### New lessons | File | Category | Surfaced by | |---|---|---| | `cherry-pick-from-sibling-testbed.md` | best-practices | Whole campaign — fetched the post-filter sibling, picked 3 fix commits directly | | `bench-dashboard-acceptance-script-parity.md` | architecture-patterns | Bug #4 — dashboard parsed banners by exact-string match; 9-of-17 gates rendered | | `test-env-hermeticity-for-backend-precedence.md` | conventions | Bug #7 — `CODEHUB_EMBEDDING_*` precedence chain leaked from operator's shell | | `parallel-docs-subagent-overscrubs-adrs.md` | best-practices | The docs subagent stripped AC-* from `docs/adr/0013-m7` and `0014` despite PR #74's ADR carve-out — required a follow-up restore commit | ### Updated - `no-spec-coordinate-leakage-into-source.md` — added a "Sweep scope is `packages/` and `scripts/`, NOT `docs/adr/*`" rule that names PR #74's carve-out, so future subagents reading the lesson see the constraint without PR archaeology. - `INDEX.md` — pointers for the four new lessons. ## Test plan - [ ] CI green on `chore/v1-compound-lessons` - [ ] No spec-coordinate leakage in source: `rg -n 'AC-[A-Z]-[0-9]' packages/ scripts/` returns zero hits. - [ ] Future ERPAVal sessions that load `INDEX.md` at session start surface these four lessons.
1 parent 1b6f22e commit 60eef57

8 files changed

Lines changed: 300 additions & 5 deletions

File tree

.erpaval/INDEX.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ development sessions. Solutions are reusable; specs are per-feature.
3434
- [No spec-coordinate leakage into source](solutions/best-practices/no-spec-coordinate-leakage-into-source.md) — ERPAVal `AC-*`, `M-*`, `W-*`, `CL-*` prefixes belong in commits, PR bodies, ADR refs sections — NOT in JSDoc, inline comments, CLI flag help, MCP tool descriptions, or test names. Sweep `rg -n "AC-[A-Z]-[0-9]" packages/` before every PR-open; LLM clients pick up the leakage and start citing it back.
3535
- [release: published events need PAT or inline](solutions/conventions/release-published-event-needs-pat-or-inline.md) — release-please-action with default `GITHUB_TOKEN` does NOT fire downstream `release: [published]` workflows; inline asset-attach in `release-please.yml` gated on `steps.release.outputs.release_created`. Fixed AC-D-4; sbom.yml has same latent bug for follow-on.
3636
- [Dogfood pre-push hook catches CLI spec drift on first push](solutions/best-practices/dogfood-prepush-hook-caught-cli-spec-mismatch.md) — the first `git push` of the commit that adds a self-targeting pre-push hook is where spec/CLI-flag mismatches and "missing index" foot-guns surface. Pattern: SKIP-with-message shape from `pack-determinism-audit.sh` for any gate that depends on a derived artifact.
37+
- [Cherry-pick verified bug fixes from a sibling testbed clone](solutions/best-practices/cherry-pick-from-sibling-testbed.md) — when a post-filter sibling has authored fix commits with file:line repro coordinates, fetch the sibling and cherry-pick directly; preserves authorship, halves review surface, defeats re-author drift.
38+
- [Bench dashboard ↔ acceptance script banner-text parity](solutions/architecture-patterns/bench-dashboard-acceptance-script-parity.md) — when a dashboard parses banners by exact-string match, the two artifacts must be edited together; add a roster-shape test that pulls the banner list from the script directly. Surfaced 9-of-17 gates rendering by Bug #4 in 2026-05-10 smoke campaign.
39+
- [Test env hermeticity for backend-precedence libraries](solutions/conventions/test-env-hermeticity-for-backend-precedence.md) — when an SDK picks a backend by env presence, tests must scope-stash every key in the chain via prefix glob, not just the one they assert on. Per Bug #7 in 2026-05-10 smoke: `CODEHUB_EMBEDDING_*` chain.
40+
- [Parallel docs subagents over-scrub ADR coordinates](solutions/best-practices/parallel-docs-subagent-overscrubs-adrs.md) — PR #74's carve-out for ADR text isn't visible in the durable lesson; brief docs subagents explicitly that `docs/adr/*` retains spec coordinates.
3741

3842
## Specs
3943

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
name: A dashboard that parses banner-text from a script must mirror the script's banners verbatim
3+
description: Bench/dashboard tools that index gates/jobs by exact-title match against a script's banner output drift silently when the script grows new gates — both files must be edited together
4+
type: architecture-patterns
5+
---
6+
7+
`packages/cli/src/commands/bench.ts` indexes gate rows by exact-string
8+
match against `scripts/acceptance.sh` banners (`N/17: <title>`). When
9+
the script grew from 9 to 17 gates and changed a few existing banner
10+
titles ("graphHash determinism" → "determinism (double-run graphHash)"),
11+
the dashboard didn't follow. Result: 8 gates never advance past
12+
"pending" and post-stream get stamped "skipped — script crashed" by the
13+
crash-fallback path; another 3 displayed under stale titles. Operators
14+
saw 9/17 gates with confusing detail strings.
15+
16+
The original code shape:
17+
18+
```ts
19+
export const MVP_GATES: readonly { id: string; title: string }[] = [
20+
{ id: "install", title: "pnpm install --frozen-lockfile" },
21+
// ... 8 more, with stale titles
22+
];
23+
24+
export function applyLine(rows: GateRow[], rawLine: string): void {
25+
const banner = /^\d+\/\d+:\s+(.*)$/.exec(line);
26+
if (banner) {
27+
const idx = rows.findIndex((r) => r.title === banner[1]); // exact match
28+
if (idx >= 0) currentGateIdx = idx;
29+
}
30+
}
31+
```
32+
33+
**Why:** the dashboard is a thin presenter over the script's stdout. Any
34+
banner text not in `MVP_GATES` is silently dropped. There is no compile-
35+
time signal — the build is green, the unit tests are green, only the
36+
runtime UX degrades. The same gap also caught `[SKIP]` markers: the
37+
original `applyLine` matched `[PASS]`/`[FAIL]` but not `[SKIP]`, so
38+
gracefully-degrading gates rendered as "skipped — script crashed" via
39+
the crash-fallback path with a misleading detail string.
40+
41+
**How to apply:**
42+
43+
1. **Treat banner titles as a contract** between the script and the
44+
dashboard. Edit both files in the same commit.
45+
2. **Add a roster-shape test.** Assert `MVP_GATES.length === 17` AND
46+
`MVP_GATES.map(g => g.title)` matches the banner sequence the script
47+
emits. The test pulls the banner list from the script directly with
48+
`grep -oE '^echo "\d+/\${TOTAL_GATES}: (.+)"$' scripts/acceptance.sh`
49+
so the assertion follows the source of truth.
50+
3. **Match every marker the script emits.** If the script emits `[PASS]`,
51+
`[FAIL]`, AND `[SKIP]`, the parser must handle all three. The
52+
crash-fallback path must NOT fire for legitimate skips.
53+
4. **Order matters when index = listr2 row.** `MVP_GATES` order must
54+
match script execution order — the dashboard advances rows by index
55+
as banners stream in.
56+
57+
Anti-pattern: a "we'll keep them in sync manually" comment without an
58+
enforcement test. The 9-gate / 17-gate drift sat in `main` undetected
59+
because no CI surface failed when the script grew. Surfacing it
60+
required an operator to run `codehub bench` and notice the visual
61+
mismatch.
62+
63+
Cross-link: the `dogfood-prepush-hook-caught-cli-spec-mismatch` durable
64+
lesson covers a related pattern — the dogfood pre-push hook on this
65+
exact PR was where this bug was first surfaced (Bug #4 in
66+
UPSTREAM_BUGS.md, 2026-05-10 smoke).
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
name: Cherry-pick verified bug fixes from a sibling testbed clone
3+
description: When a sibling/post-filter checkout has authored fix commits with file:line repro coordinates, fetch the sibling and cherry-pick directly — no need to re-author or re-test on upstream
4+
type: best-practices
5+
---
6+
7+
When you maintain a "post-filter testbed" sibling repo for smoke / dogfood
8+
campaigns and you've already authored fix commits there with verified
9+
repros, do not re-write the fixes on upstream. Fetch the sibling as a
10+
local remote and cherry-pick.
11+
12+
**Why:** The fix has already been authored, repro'd, verified. Re-authoring
13+
on upstream loses authorship metadata, doubles review surface, and
14+
introduces drift between what was fixed and what landed. Re-testing
15+
re-validates the same green path. The cherry-pick is provably equivalent
16+
when the file:line coordinates in the fix message match upstream HEAD.
17+
18+
**How to apply:**
19+
20+
1. **Verify file:line parity first.** Each fix in the testbed report
21+
should cite file paths and line numbers; quickly grep upstream to
22+
confirm the same lines exist there. Per Bug #2 in OCH 2026-05-10
23+
campaign: `packages/cli/src/commands/scan.ts:162-171` was identical in
24+
testbed and upstream — direct cherry-pick worked.
25+
2. **Fetch the sibling as a path remote.** No need to register it
26+
permanently. One-shot:
27+
```bash
28+
git fetch /efs/lalsaado/workplace/opencodehub.post-filter --no-tags
29+
```
30+
`FETCH_HEAD` now points at the sibling's HEAD; commits referenced by
31+
short-hash become resolvable.
32+
3. **Cherry-pick in severity order.** HIGH first, MEDIUM next, LOW last.
33+
Each pick is one commit; do not squash them into a "umbrella fix"
34+
commit — preserves blame and lets the PR reviewer see one
35+
self-contained fix per scope.
36+
4. **Re-verify after each pick** with the package-scoped check:
37+
`pnpm -F @opencodehub/<pkg> test` plus any smoke script the fix
38+
targets (`bash scripts/smoke-mcp.sh`, `node ... doctor`, etc.).
39+
5. **Prefer one PR for the bundle** when the fixes are small and
40+
thematically related (a "v1 upstream bug sweep") — reviewer context
41+
stays coherent. Split only if the bundle exceeds reviewability.
42+
43+
Anti-pattern: re-authoring the fix on upstream and citing the testbed
44+
commit in the body. That loses the original commit's authorship and
45+
makes blame point at the re-author for code that was thought-through
46+
elsewhere. If you need to adapt the fix to upstream divergence, do that
47+
as a follow-up commit on top of the cherry-pick, not a rewrite.
48+
49+
Related: this pairs naturally with the durable lesson "Squash-merge
50+
masks pre-existing repo-wide debt" — run `mise run check` on upstream
51+
BEFORE the cherry-pick to baseline-clean, so any test regression after
52+
the pick is unambiguously attributable to the picked fix.

.erpaval/solutions/best-practices/no-spec-coordinate-leakage-into-source.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,19 @@ LLM clients then pick up and start citing back; the leakage compounds.
4848
("graphHash parity: medium-with-empty-keywords ([] vs absent)") over
4949
the AC ("AC-C-2: graphHash..."). The behavior survives renames; AC
5050
numbers don't.
51-
- **Sweep before commit.** Run `rg -n "AC-[A-Z]-[0-9]" packages/`
51+
- **Sweep before commit.** Run `rg -n "AC-[A-Z]-[0-9]" packages/ scripts/`
5252
against your branch before PR-open. Anything that hits is a
5353
candidate for rephrase. If the comment NEEDS to cite the AC, use a
5454
short reference at the end like "(AC-C-5)" rather than leading with
5555
it.
56+
- **Sweep scope is `packages/` and `scripts/`, NOT `docs/adr/*`.** PR #74
57+
(`f09d804`) carved out `docs/adr/*` as the explicit place where
58+
coordinates ARE permanent decision rationale. A docs-refresh subagent
59+
that sees the sweep regex without the scope qualifier will scrub
60+
ADRs by default — DO NOT. Brief docs subagents explicitly that ADR
61+
text retains coordinates. See the
62+
`parallel-docs-subagent-overscrubs-adrs.md` lesson for the failure
63+
mode.
5664
- **The test fakes are the trap.** When a Wave subagent edits a test
5765
fake, it tends to add `// AC-XXX: stubs ...` because it's writing
5866
the comment WITH the AC packet open in front of it. Sweep test files
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
name: Parallel docs-refresh subagents must be told that ADR text is the carve-out where spec coordinates ARE allowed
3+
description: When a docs-refresh subagent inherits the "no spec-coordinate leakage" rule from durable lessons, it will scrub ADR text by default — but PR #74 carved out docs/adr/* as the place where coordinates ARE the durable rationale; brief explicitly
4+
type: best-practices
5+
---
6+
7+
OCH PR #74 (`f09d804 chore(repo): scrub ERPAVal spec coordinates from
8+
source`) explicitly retained spec coordinates in `docs/adr/*` as
9+
"permanent decision rationale". The durable lesson
10+
`no-spec-coordinate-leakage-into-source.md` documents the scrub but
11+
does NOT crisply state the carve-out. When a parallel docs-refresh
12+
subagent reads the durable lesson and is told "no spec-coordinate
13+
leakage", it scrubs ADRs too — undoing PR #74's deliberate carve-out.
14+
15+
Observed in OCH session 6c091d (2026-05-10 v1 upstream bug sweep): the
16+
docs-refresh subagent stripped `AC-A-1`, `AC-A-2`, `AC-A-6 a/b/c/d`,
17+
`AC-A-7`, `AC-A-9`, `AC-A-11` from ADR 0013-m7 and `AC-C-3`, `AC-C-5`,
18+
`E-C-3`, `W-A-2` from ADR 0014. Required a follow-up
19+
`docs(docs): restore ADR-permanent spec coordinates per PR #74 policy`
20+
commit on the same branch.
21+
22+
**Why:** the durable lesson's scope says "production source, JSDoc,
23+
inline comments, CLI flag help, MCP tool option descriptions, test
24+
names" — but the ADR carve-out lives only in PR #74's body. Subagents
25+
read the lesson, not the PR archive. The carve-out is invisible to a
26+
fresh agent.
27+
28+
**How to apply:**
29+
30+
1. **Brief docs subagents explicitly.** When seeding a docs-refresh
31+
subagent prompt, include both rules:
32+
- "No spec-coordinate prefixes in production source (per durable
33+
lesson)."
34+
- "ADR text is the carve-out: spec coordinates in `docs/adr/*` are
35+
intentional permanent rationale per PR #74. Do NOT scrub them
36+
there."
37+
2. **Update the lesson itself.** Edit
38+
`solutions/best-practices/no-spec-coordinate-leakage-into-source.md`
39+
to add a "Scope" section that names `docs/adr/*` as the carve-out,
40+
so future subagents reading the lesson see the constraint without
41+
needing PR archaeology.
42+
3. **Sweep with a scope-aware regex.** When auditing leakage, exclude
43+
`docs/adr/*` from the sweep:
44+
`rg -n 'AC-[A-Z]-[0-9]' packages/ scripts/`
45+
not
46+
`rg -n 'AC-[A-Z]-[0-9]'` (which would falsely flag ADRs).
47+
4. **The reverse case is also valid.** `docs/adr/0014-*` originally
48+
listed `.erpaval/specs/...` and `.erpaval/sessions/...` as
49+
References — those paths are gitignored and rot once the packet
50+
graduates. Replacing them with code-path citations IS correct, even
51+
in ADR text. The carve-out is for spec-coordinate prefixes, not for
52+
pointers to gitignored paths.
53+
54+
Anti-pattern: writing a generic "scrub spec coords everywhere" rule and
55+
then surprised when ADR rationale gets vacuumed. The leakage rule
56+
exists to prevent rot; ADR rationale doesn't rot because the ADR is
57+
the rationale.
58+
59+
Cross-link:
60+
[no-spec-coordinate-leakage-into-source](no-spec-coordinate-leakage-into-source.md) — the original rule.
61+
PR #74 (`f09d804`) — the carve-out's authoritative source.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
name: Tests for backend-precedence libraries must wipe all env keys in the precedence chain, not just the one they assert
3+
description: When an SDK picks a backend by env presence (CODEHUB_EMBEDDING_SAGEMAKER_ENDPOINT, CODEHUB_EMBEDDING_URL, ...), tests of "backend X is picked when only X's env is set" must scope-stash every key in the chain, not only the local one
4+
type: conventions
5+
---
6+
7+
`packages/embedder/src/http-embedder.test.ts:441,458` asserted that
8+
`tryOpenHttpEmbedder` returns `null` when its specific env var is unset.
9+
The test only stashed `CODEHUB_HOME`. With
10+
`CODEHUB_EMBEDDING_SAGEMAKER_ENDPOINT` exported in the operator's shell,
11+
the higher-precedence SageMaker backend short-circuited, the assertion
12+
flipped, and the test failed — but only on the specific dev box where
13+
the operator was working through SageMaker integration.
14+
15+
The fix: a `sanitizeEmbeddingEnv()` helper that snapshots and wipes
16+
every `CODEHUB_EMBEDDING_*` key plus `CODEHUB_HOME`, restored on
17+
teardown via `beforeEach`/`afterEach`:
18+
19+
```ts
20+
function sanitizeEmbeddingEnv() {
21+
const saved: Record<string, string | undefined> = {};
22+
for (const k of Object.keys(process.env)) {
23+
if (k.startsWith("CODEHUB_EMBEDDING_") || k === "CODEHUB_HOME") {
24+
saved[k] = process.env[k];
25+
delete process.env[k];
26+
}
27+
}
28+
return () => {
29+
for (const [k, v] of Object.entries(saved)) {
30+
if (v === undefined) delete process.env[k]; else process.env[k] = v;
31+
}
32+
};
33+
}
34+
```
35+
36+
**Why:** the backend-precedence pattern is a chain — env-X-set → backend-X,
37+
else env-Y-set → backend-Y, else fallback. A test that asserts about
38+
backend Y must explicitly clear backend-X's env, otherwise the assertion
39+
silently tests the wrong code path under any operator who happens to
40+
have backend-X configured. The failure is non-reproducible on a clean
41+
laptop, fires on a dev box with the higher-precedence env exported.
42+
This is exactly the env-leak class that bedevils CI-vs-local divergence
43+
debugging.
44+
45+
**How to apply:**
46+
47+
1. **Identify the precedence chain.** For OCH embedder:
48+
`CODEHUB_EMBEDDING_SAGEMAKER_ENDPOINT``CODEHUB_EMBEDDING_URL`
49+
`CODEHUB_EMBEDDING_*` (HTTP options) → `CODEHUB_HOME` (local ONNX).
50+
Any test that asserts about backend selection must wipe the entire
51+
chain, not just one key.
52+
2. **Stash with a prefix glob, not a fixed key list.** `Object.keys`
53+
filtered by `startsWith("CODEHUB_EMBEDDING_")` catches keys added
54+
later (e.g. a future `CODEHUB_EMBEDDING_AZURE_*`) without revisiting
55+
every test.
56+
3. **Wire it as `beforeEach`/`afterEach`, not per-case try/finally.**
57+
Easier to audit; harder to forget on the next case.
58+
4. **Apply defensively to sibling describe blocks.** Even cases that
59+
don't care about the env can be poisoned by stale state from a prior
60+
test that mutated `process.env`. Hermetic test suites don't pay a
61+
cost for being defensive.
62+
63+
Anti-pattern: per-case `originalKey = process.env[KEY]; ... finally
64+
process.env[KEY] = originalKey` for a single key. The single-key save
65+
worked when there was one env var; with a chain, every test that misses
66+
a sibling key in the chain becomes flaky on operator boxes.
67+
68+
Cross-link: pairs with the existing `sagemaker-embedder-backend.md`
69+
durable lesson — that one covers the SDK-side dynamic-import + soft-fail
70+
pattern; this one covers the test-side env-hermeticity pattern that
71+
that pattern requires.

.github/workflows/ci.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,34 @@ jobs:
9090
--onlyAllow 'Apache-2.0;MIT;BSD-2-Clause;BSD-3-Clause;ISC;CC0-1.0;BlueOak-1.0.0;0BSD'
9191
--excludePrivatePackages
9292
--production
93+
94+
# GitHub code-scanning advanced-setup is configured to look for an `osv`
95+
# job in `ci.yml`. The standalone `osv.yml` workflow does the same scan,
96+
# but the configured pointer lives here, so we mirror it: install
97+
# osv-scanner, emit SARIF, upload to code-scanning, then fail the run on
98+
# vulnerabilities. The standalone workflow remains for the weekly cron.
99+
osv:
100+
runs-on: ubuntu-latest
101+
permissions:
102+
contents: read
103+
security-events: write
104+
steps:
105+
- uses: actions/checkout@v6
106+
- name: Install osv-scanner
107+
run: |
108+
curl -sL -o /tmp/osv-scanner \
109+
https://github.com/google/osv-scanner/releases/download/v2.3.8/osv-scanner_linux_amd64
110+
chmod +x /tmp/osv-scanner
111+
- name: Scan pnpm-lock.yaml (SARIF output)
112+
run: |
113+
/tmp/osv-scanner scan source \
114+
--lockfile=pnpm-lock.yaml \
115+
--format=sarif \
116+
--output=osv.sarif || true
117+
- uses: github/codeql-action/upload-sarif@v4
118+
if: always()
119+
with:
120+
sarif_file: osv.sarif
121+
category: osv-scanner
122+
- name: Fail on vulnerabilities
123+
run: /tmp/osv-scanner scan source --lockfile=pnpm-lock.yaml

.github/workflows/osv.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
name: OSV-Scanner
22

3+
# The push/pull_request runs were moved to `ci.yml`'s `osv` job so the
4+
# GitHub code-scanning advanced-setup configuration (which is pinned to
5+
# `ci.yml:osv`) finds them. This standalone workflow keeps the weekly
6+
# cron + manual dispatch path so OSV advisory-data updates are picked
7+
# up between PRs.
38
on:
4-
push:
5-
branches: [main]
6-
pull_request:
7-
branches: [main]
9+
workflow_dispatch:
810
schedule:
911
- cron: "33 5 * * 2"
1012

0 commit comments

Comments
 (0)