Files
msd-core/.changeset
Tom Boucher 2c54f219c9 fix(#2348): derive verification staleness from git commit time, not mtime (#2394)
readVerificationStatus() decided a phase's verification was `stale` (a
*-SUMMARY.md newer than the *-VERIFICATION.md) by comparing filesystem
mtimes. mtimes are assigned at checkout time and are not preserved by
`git clone` / `cp -R`, and any unrelated `touch` / reformat / editor-save
re-stales a valid report — so a committed phase declaring `status: passed`
could silently read `stale` on a fresh clone purely from checkout order,
falsely rewriting a ROADMAP row and blocking milestone close (#2022 gate).

Each file's effective "last changed" time is now its git commit time when
the file is committed AND clean, and its mtime otherwise (uncommitted or
working-tree-dirty). Both are real wall-clock change times, so a summary
committed after — or edited after — the verification reads stale, while a
clean fresh clone stays passed. Git commit time is content-tied and clone-
stable; mtime is retained only where it is the true last-changed signal.

Implementation:
- Two bounded git calls per phase (never one-per-file): `git log
  --first-parent --format=%ct --name-only` for commit times, and `git diff
  --name-only HEAD` to drop dirty files. readVerificationStatus runs
  per-phase in the init/roadmap listing loops, so per-file spawning would
  fan out to P×(S+1) git processes ("Unbounded Subprocesses").
- `--first-parent` so merge commits report their file lists (plain
  `--name-only` omits merge diffs and would under-date merge-landed content).
- The dirty-check fails SAFE: if `git diff` is inconclusive (errors / exits
  non-zero) the commit times are discarded so every file falls back to mtime,
  never trusting a possibly-stale commit time (no false "not stale").
- Paths matched back by `/`-bounded suffix (root vs nested `plans/` can't
  collide) and passed after `--` (dash-named files can't be read as flags).
- A phase with no summaries skips git entirely; the scan short-circuits on
  the first stale summary.

A `phaseCleanCommitTimesMs` seam keeps the unit tests hermetic (no git
spawn); the resolver's two-call error handling is unit-tested via an
injected execGit; two real-git integration tests lock the end-to-end path,
the committed-then-edited (dirty) regression, and the `--` argv guard.
2026-07-17 21:58:02 -04:00
..

Changeset Fragments

This directory holds per-PR CHANGELOG fragments. Every PR with user-facing changes drops one (or more) <random-name>.md files here describing its CHANGELOG entry. Fragments are consolidated into the top-level CHANGELOG.md at release time.

Why

Two PRs that both edit the ### Fixed block of CHANGELOG.md always conflict on merge — git can't pick a serialization order without human input. Two PRs that each add a fresh .changeset/<unique-name>.md never conflict because they don't share lines.

See #2975 for the full rationale.

Adding a fragment

node scripts/changeset/new.cjs \
  --type Fixed \
  --pr 1234 \
  --body "fix the thing — explain the user-visible change in one sentence"

This writes .changeset/<adjective>-<noun>-<noun>.md with frontmatter and a body. Three random words → concurrent PRs don't collide.

Format

---
type: Fixed
pr: 1234
---
**`/gsd-foo` no longer drops trailing slashes** — explain the user-visible change.

Allowed type: values follow Keep a Changelog: Added, Changed, Deprecated, Removed, Fixed, Security.

Opting out

PRs that legitimately have no user-facing impact can add the no-changelog label. CI honors it. When unsure, add the fragment.

At release time

Promotion is automatic. The release workflow's finalize job runs:

node scripts/changeset/cli.cjs render --version vX.Y.Z --date YYYY-MM-DD --allow-empty

This reads every fragment, groups bullets by type:, replaces ## [Unreleased] with a new ## [vX.Y.Z] - YYYY-MM-DD block, opens a fresh ## [Unreleased] above, and deletes consumed fragments. The --allow-empty flag ensures a no-change release still gets a dated heading (with a _No notable changes._ placeholder). A subsequent verify step confirms the promotion landed correctly. Maintainers do not run this by hand.

Archived fragments

.changeset/archived/ holds fragments for already-shipped releases (≤ 1.3.1), retained for provenance. Their content was hand-curated into the dated ## [1.x.y] sections of CHANGELOG.md during the #690 backfill — they were never consumed by render. All changeset tooling enumerates .changeset/ non-recursively, so archived fragments are never picked up or rendered. Do not move them back to the top level.