Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/agent-memory/product-architect/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- [Source-report split inference](source-report-split-inference.md) — budgetLines[]/deposits[] are this-source-scoped so array-shape gates are proxies; **`splitKind` SHIPPED #1911/PR #2015** incl. the ≠S-per-arm predicate, the residual arithmetic proving `(less deposit)` in both directions, the UNION-dedup/`COUNT(*)` trap, and why `isSplit` must be retained as an independent cross-check; pdfmake `'2*'` width trap; wiki + shared type JSDoc fixed (#1914, #1917/PR #1994)
- [Story reviews](story-reviews.md) — per-story and per-PR review log
- [Client PDF pipeline](client-pdf-pipeline.md) — ADR-034 report PDF generation, reportContent content/layout split (#1900), `dontBreakRows` silent-drop rule, document-level deduplicated legend (#1965) — ADR-034 B4 rule + legend addendum landed in PR #1979; per-locale header character budget + "no interface `t` in header/footer" (#1937/#1938, PR #1982); pdfmake `Content` is unspreadable (TS2698) but `Object.assign` needs no cast, and per-item `wordBreak`/newline-only-run facts (#1968, PR #2002). **ADR-034 debt fully PAID 2026-08-04 (#1914)**: width rule #1 (`max(horizontalRatio) <= 1`, not `_minWidth`), module table, override keys, dontBreakRows/height-bound section, injection-only locale contract. `merge.ts` footer/header interface-`t` defect **CLOSED** (footer PR #2000/#1993, header #1938); `TFunction` removed from all of `reportPdf/*` in #2001/PR #2007, so the locale contract is now compiler-enforced there and `buildReportContent.ts` is the single legitimate `TFunction` entry point — ADR-034 lines 82/178/186-188/200/219 all state the old weaker contract and needed a wiki pass (flagged in the PR #2007 review). **ADR-034 rule #1 is WRONG A SECOND TIME (proven in the PR #2008 review, 2026-08-05): `horizontalRatio` is a cell-origin bound, not a content-extent check — it is unconditionally `<= 1` on the all-fixed-width production table and cannot detect token overflow. The check that works is per-cell `_minWidth <= _calcWidth` (the ADR's `wordBreak` false-positive rationale for banning `_minWidth` is empirically false). **Third correction PAID 2026-08-05** (wiki `da1324b`): rule #1 leads with per-cell `_minWidth`, `maxHorizontalRatio` re-scoped to table-box positioning with a vacuity warning, `_minWidth` ban split table-level vs per-cell, 3rd Deviation Log row added. Semantics verified by mutation test in the PR #2008 round-2 review; **two follow-ups still open on the page — every quoted figure (69.28/33.54/266.16pt) is stale, and the rule says "every cell" while the test covers only Usage.** **#1973/PR #2010 (2026-08-05, CHANGES REQUESTED): the 2-hardcoded-shapes era is over — 96 legal column subsets via a single-absorber `computeColumnWidths`, `reportContent/columns.ts` as the AC 2.1 single derivation; `USAGE_WIDTH_7COL` 138.28pt is the NARROWEST Usage width so the 650 chunk budget needed no re-measurement (one-sided clamp). ADR-034's "Geometry constraint that blocks a feature" (line 153) was a false prohibition, plus line 144's constant reference and line 155's "hangs" claim (it throws). **All three PAID by me 2026-08-05, wiki `eb24774`**: section replaced with "Column geometry is a computed engine, not two pinned shapes", `columns.ts` added to the module structure, Deviation Log row. Same commit documented the wizard **tier-factory convention** in Architecture.md (never documented anywhere before, born #1947 after #1943/#1946) incl. the "second `DISCARD_EDITS` opt-out must trigger a tier split" limit.** **#1912/PR #2028 (2026-08-06, APPROVED): required `reportFormatters` is ADR-034 line 230's own "remove the channel" principle, and line 248 already documented the 6-arg signature — no Deviation Log row, but invariant 1 (line 206) now under-claims and wants one sentence on the next ADR-034 pass, alongside a one-liner naming `Formatters` (PDF injection contract) vs `AppFormatters extends Formatters` (app-wide superset). Open medium: `buildReportContent.ts` still has 4 unguarded union-into-`reportT()` keys (lines 143/146/204/274) rendering into the bank PDF — see recurring-patterns "1 of N sites".** **#1940/PR #2032 (2026-08-06, APPROVED): runt-merge gate + `'… '` continuation marker; the backward-merge induction (receiver virginity is structural) verified by hand AND a 400k-case verbatim-port fuzz; ADR-034 line 152's call-site quote stale a THIRD time and line 148's "bound what a cell renders" rule now under-satisfied by the 2-char unbounded marker (safe by size only: worst case +1 line, 41->42 vs the 44-line budget). #1950 needs no reorder, but its guard pins a rendered quantity that the marker moves to 36 chars / 4 lines / 44.8pt on continuation rows.** **#1941/PR #2033 (2026-08-06, APPROVED, comment not `--approve` — own PR): editor-side input caps land as module constants in `ReportContentEditor.tsx`, `USAGE_TEXT_MAX_LENGTH = 500` against the computed 650. **ADR-034 pass gains a FIFTH item**: line 148's "bound what a cell renders" now needs an input-side counterpart sentence — "input caps sit below the render budget so a typed value never needs a continuation row on its own; the render budget remains the only correctness bound." Also routed to #1950: extend its guard to `USAGE_TEXT_MAX_LENGTH < usageChunkCharsForWidth(USAGE_WIDTH_7COL)` (needs the constant exported; keep it in the editor — an input constraint in the renderer inverts the dependency). NOT a guard on `500 + 150 <= 650`: that arithmetic is a fiction, see recurring-patterns.**
- [Release toolchain](release-toolchain.md) — ccc preset major must match the conventional-changelog-writer engine (hold ccc at v9 until release-notes-generator@15); empty release notes hid for months; a crash-fix can restore the _silent_ bug, judge on output not on the error going away (#2082)
- [CI Jest shard tuning](ci-test-job-tuning.md) — CI runners are ~1.8x slower than the dev box; maxWorkers/shardTotal/coverage are non-levers; diagnose by last-PASS timestamp; where testTimeout belongs
- [Diary drafts pattern](diary-drafts-pattern.md) — ADR-022 draft lifecycle via status column on parent table
- [EPIC-03 refinement](epic03-refinement.md) — 40 consolidated refinement items
Expand Down
73 changes: 73 additions & 0 deletions .claude/agent-memory/product-architect/release-toolchain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
name: release-toolchain
description: semantic-release changelog toolchain constraint (conventional-changelog preset major must match the writer engine), and why a crash-fix must be judged on output not on the error going away
metadata:
type: project
---

# Release toolchain: preset major must match the writer engine

`conventional-changelog-conventionalcommits` (the `preset: "conventionalcommits"` in
`.releaserc.json`) must stay on the **v9 line** while `semantic-release@25` is in use.

**Why:** there are two incompatible rendering engines.

- `conventional-changelog-writer@8` is **handlebars**-based and expects `mainTemplate` /
`headerPartial` / `commitPartial` / `footerPartial` as **template strings**.
- `conventional-changelog-writer@9` dropped handlebars entirely for
`@conventional-changelog/template`, whose partials are **JS functions**.

`@semantic-release/release-notes-generator@14` (what `semantic-release@25.0.9` ships) pins
`conventional-changelog-writer@^8`. The ccc **v10** preset emits function partials for the
writer@9 engine. Feeding function partials to the handlebars writer does not throw — it renders
**header-only, empty release notes**. From ccc `10.3.0` upstream planted a deliberate poison-pill
`mainTemplate` (`@conventional-changelog/template`'s `createLegacyWriterGuard`) whose _doc comment
says_ it exists to "fail loudly instead of silently rendering an empty changelog"; under handlebars
it trips `helperMissing` and produces
`Missing helper: "conventional-changelog-conventionalcommits requires conventional-changelog-writer@9 or newer"`.

Upstream's own remedy text is "Update the tooling **or use an older major version of the preset**."
The real upgrade path is `@semantic-release/release-notes-generator@15` (writer@9, and it _also_
bumps `conventional-commits-parser` 6->7 and `conventional-commits-filter` 5->6 — writer alone is
not the whole migration, which is why a lone `overrides` forcing writer@9 is an untested triple).
As of 2026-09-08 rng 15 is only `15.0.0-beta.2`. A `.github/dependabot.yml` ignore rule holds ccc
at the v9 major; remove it when rng 15 is adopted.

**How to apply:** treat the ccc major and the writer major as one coupled decision. Any bump of
`semantic-release`, `@semantic-release/release-notes-generator`, or
`conventional-changelog-conventionalcommits` must be validated by a `semantic-release --dry-run`
that **prints non-empty release notes** — not merely by the absence of an error.

## The methodological trap this hid behind (issue #2082)

The empty-notes bug ran undetected for **months**: every beta release body was ~115 characters
(the compare-link header and nothing else), and the stable releases only looked healthy because
the workflow's "Enrich release notes with summary" step prepends the hand-written
`RELEASE_SUMMARY.md`. The auto-generated half of `v2.14.0` was also just the header.

Two generalizable rules came out of it:

1. **A fix that removes a crash may only restore the _silent_ version of the same bug.** The
obvious remedy here (pin ccc back to `10.2.1`, the version that last "released successfully")
makes `generateNotes` exit 0 — and still emits an empty changelog, because 10.2.1 is also a
writer@9-era preset. Verifying "the error is gone" would have shipped the bug back. The check
that discriminates is the _artifact_: `gh api repos/.../releases --jq '.[] | "\(.tag_name) \(.body|length)"'`.
See [[recurring-patterns]] — this is the release-infra instance of "assertions that pass on nothing".
2. **A CI step that parses stdout and discards the exit code cannot distinguish a crash from a
no-op.** `OUTPUT=$(npx semantic-release 2>&1) || true` plus `grep -q "Created tag v"` made a
hard failure look identical to "no releasable commits": job green,
`new-release-published=false`, and all six downstream jobs skipped. Whenever a step's success
is inferred from log text, the exit code must still gate it, and the "nothing to do" summary
must not be reachable from the failure path.

## Verifying release changes without a working `npm install`

`npm install` fails with nondeterministic `ENOTDIR` in the sandbox worktrees (different path each
attempt; the host mount sits at ~98% full). A faithful substitute: install `semantic-release` plus
the preset into a throwaway `/tmp` prefix, then run that binary with **cwd = the repo worktree**.
`@semantic-release/release-notes-generator` resolves the preset via
`importFrom.silent(__dirname, ...)` first, so the harness's copy is used, while git history,
`.releaserc.json` and the remote all come from the real repo. Export `GITHUB_ACTIONS=true`,
`GITHUB_EVENT_NAME=push`, `GITHUB_REF=refs/heads/beta` and a token so `env-ci` resolves the branch
and `verifyConditions` passes — otherwise `--branches <name>` is rejected for a branch that does
not exist on the remote, and the run never reaches `generateNotes`.
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ updates:
# (see PR #1882). Remove once ts-jest widens its peer range.
- dependency-name: 'typescript'
update-types: ['version-update:semver-major']
# Hold conventional-changelog-conventionalcommits at v9 until semantic-release
# ships a release-notes-generator built on conventional-changelog-writer@9.
# The v10 preset emits JS-function partials for the writer@9 rendering engine,
# but @semantic-release/release-notes-generator@14 pins the handlebars-based
# writer@8. That mismatch produced header-only (empty) release notes silently
# from v10.0.0, and from v10.3.0 upstream added a deliberate guard that turns
# it into a hard `generateNotes` crash (see issue #2082). Remove this rule once
# @semantic-release/release-notes-generator@15 (writer@9) is released and adopted.
- dependency-name: 'conventional-changelog-conventionalcommits'
update-types: ['version-update:semver-major']

# GitHub Actions
- package-ecosystem: github-actions
Expand Down
30 changes: 28 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,24 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
OUTPUT=$(npx semantic-release 2>&1) || true
# Capture stdout+stderr for the tag/version parsing below, but keep the
# exit code: a crash and a clean "no relevant commits" run are otherwise
# indistinguishable, because both simply lack "Created tag v" in stdout.
# Swallowing the status with `|| true` let a hard generateNotes failure
# report success with new-release-published=false, silently skipping every
# downstream job — that is how v2.15.0 was never published (issue #2082).
# Never discard this status.
set +e
OUTPUT=$(npx semantic-release 2>&1)
STATUS=$?
set -e
echo "$OUTPUT"

if [ "$STATUS" -ne 0 ]; then
echo "::error title=semantic-release failed::semantic-release exited with status ${STATUS}. No release was published; see the step log above for the failing plugin step."
exit "$STATUS"
fi

if echo "$OUTPUT" | grep -q "Created tag v"; then
VERSION=$(echo "$OUTPUT" | grep -oP 'Created tag v\K[0-9]+\.[0-9]+\.[0-9]+(-beta\.[0-9]+)?' || echo "")
echo "new-release-published=true" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -108,12 +124,22 @@ jobs:
echo "| **Type** | ${RELEASE_TYPE} |"
echo "| **GitHub Release** | [v${VERSION}](https://github.com/steilerDev/cornerstone/releases/tag/v${VERSION}) |"
} >> "$GITHUB_STEP_SUMMARY"
else
elif [ "$PUBLISHED" = "false" ]; then
{
echo "### No Release"
echo ""
echo "No new release — commits since last tag did not trigger a version bump."
} >> "$GITHUB_STEP_SUMMARY"
else
# semantic-release did not complete, so "no version bump" is not a
# supported conclusion — the outputs were never set. Say so plainly
# rather than reporting a clean no-op (issue #2082).
{
echo "### Release Failed"
echo ""
echo "\`semantic-release\` did not complete successfully, so no release was published."
echo "This is **not** a \"no relevant commits\" run — check the \`Semantic Release\` step log for the failing plugin step."
} >> "$GITHUB_STEP_SUMMARY"
fi

# ---------------------------------------------------------------------------
Expand Down
24 changes: 7 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"@testing-library/user-event": "14.6.7",
"@types/jest": "30.0.0",
"concurrently": "10.0.5",
"conventional-changelog-conventionalcommits": "10.4.0",
"conventional-changelog-conventionalcommits": "9.3.1",
"eslint": "10.10.0",
"eslint-config-prettier": "10.1.8",
"identity-obj-proxy": "3.0.0",
Expand Down