* enhance(#4487): stamp windows-ledger entries with the resolved milestone Broken-windows ledger entries (`.planning/WINDOWS.md`) carry `phase` as a bare number. Phase numbers are unique only within one active phases/ directory -- `milestone complete` archives phases and frees their numbers for reuse, so two milestones routinely produce entries sharing the same phase value with nothing distinguishing them. Since `/gsd-ship` blocks while any entry is open, an already-archived milestone's open entries could silently block shipping the CURRENT milestone, with no supported way to attribute which entry belonged to which milestone short of manually cross-referencing MILESTONES.md timestamps against decision IDs that happened to appear in description prose. Added an optional `milestone: string | null` field to WindowEntry, stamped by `windows append` (cmdWindowsAppend, which already does file I/O) from the workstream's resolved milestone version. Reused the existing `readCurrentMilestoneVersion` (workstream-inventory.cts -- STATE.md `milestone:` frontmatter first, ROADMAP.md in-progress marker as fallback) rather than writing a parallel implementation: exported it via that module's existing `export = {...}` CJS-interop convention (matching the `import ... = require(...)` pattern already used in workstream.cts/init.cts). appendWindow itself stays pure -- it accepts milestone as an optional input field and passes it through; only the CLI-facing cmdWindowsAppend resolves it from disk. Backward compatible by construction: validateEntryShape does NOT add `milestone` to its required fields, so an existing ledger entry with no milestone key at all parses without error and reads back as null -- exactly "recorded before this change," no migration needed. The rendered markdown table is deliberately left unchanged (the issue's own words: "the JSON is the source of truth"); adding a table column would be a separate, larger change than adding an optional JSON field. Two smaller gaps the issue itself flags as separable ("happy to split them out") are explicitly NOT addressed here: no verb to amend an entry's description, and the table/JSON drift-repair advice that can destroy table-only edits on a parse failure. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#4487): preserve absent-vs-null milestone through parse/render roundtrip validateEntryShape stamped an explicit `milestone: null` onto every entry lacking the key, so a pre-#4487 ledger entry gained permanent JSON churn ("milestone": null) the first time ANY entry in the ledger was touched -- breaking the pure parse/render roundtrip-identity property test (render(parse(render(ledger))) must equal render(ledger)) and, in real usage, contaminating unrelated entries' diffs on every append/waive/fixed of an old ledger. Fixed by distinguishing "key genuinely absent" (undefined -- JSON. stringify drops it, matching pre-#4487 behavior exactly) from "recorded but unresolvable" (explicit null, the real signal appendWindow stamps on brand-new entries). WindowEntry.milestone is now optional (`milestone?: string | null`) so returning undefined type-checks. Updated tests/broken-windows.test.cjs's roundtrip property generator to exercise all three states (absent/null/string) -- its prior silence on this field is exactly what let the regression through. Also corrected the earlier backward-compatibility test's assertion: a pre-#4487 entry reads as milestone: undefined, not null, and re-rendering it must not introduce a milestone key at all. Also ran npm run regen:derived: docs/features/broken-windows-ledger.md (edited in an earlier commit) had never been propagated to its generated docs/FEATURES.md projection, which is what was independently failing tests/features-index-gate.test.cjs and, as a side effect of staleness, tripping tests/fragment-single-edit-propagation.install. test.cjs's second-source-surface check. Manually verified via the compiled lib (500 fast-check iterations plus direct legacy/new-entry roundtrip checks) before wiring the test file, since this repo blocks local node --test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#4487): materialize milestone via conditional spread, not undefined assignment An object literal property set to `milestone: undefined` is still an OWN property -- `'milestone' in entry` reads true regardless of the assigned value, only JSON.stringify treats undefined specially. My prior commit's own new backward-compat test asserted `'milestone' in entry === false` for a pre-#4487 entry and failed on exactly this. Switched to conditionally spreading the key in only when the source object actually had it, so a genuinely absent milestone is not materialized at all -- matching both the `in` check and JSON serialization. Re-verified via the compiled lib (500 fast-check roundtrip iterations, plus the specific in/undefined/JSON assertions the failing test makes) before re-running gsd-test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs(#4487): backfill changeset pr number to 4583 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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.