5 Commits

Author SHA1 Message Date
Jakub Zych
a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00
sim
2eef8ada4f test(#4520): migrate generators/doc-gates/attribution batch to named timeout constants
Batch 9 of 17 in the ad hoc timeout literal migration (epic #4445).
Replaces every bare numeric timeout/timeoutMs object-literal property in
14 files with a named constant, per eslint-rules/no-adhoc-timeout-literal.cjs.
Removes the 14 files from the rule's allowlist.

The issue's own guess ("all run a scripts/*.cjs generator or lint script
once, BUILD_TIMEOUT_MS class") needed two corrections found by reading
every site directly. First, BUILD_TIMEOUT_MS's own doc comment scopes it
specifically to scripts/build-hooks.js, which none of this batch's
generator/lint-script sites run — a new shared constant,
GENERATOR_SCRIPT_TIMEOUT_MS, covers the class instead. Second,
no-pending-3212-markers.test.cjs's single site spawns `git ls-files`
directly, not a scripts/*.cjs script at all — routed to a second new
shared constant, REAL_REPO_GIT_TIMEOUT_MS, promoted once
emitted-attribution.test.cjs's own git-plumbing sites were found sharing
the same class and value.

Isolated Standards-axis review caught a further misclassification: one
of REAL_REPO_GIT_TIMEOUT_MS's three emitted-attribution.test.cjs sites
actually builds a fresh throwaway temp repo (createTempDir + git init),
contradicting that constant's own real-repo-tree-only scope. Fixed with
a new file-local FRESH_FIXTURE_GIT_TIMEOUT_MS holding the exact
pre-existing value under an honest name, rather than reusing the shared
GIT_FIXTURE_TIMEOUT_MS (which would have doubled the bound).

emitted-attribution.test.cjs also gets two more file-local constants:
HEAVY_REAL_TREE_TEST_TIMEOUT_MS (node:test's own per-test timeout
option, not a spawn bound) and BUILD_HOOKS_UNDER_LOAD_TIMEOUT_MS (the
same build-hooks.js script as the shared norm, at 4x its bound inside
the suite's heaviest test). emitted-ack-trailer.test.cjs gets
IMPOSSIBLY_SHORT_GIT_TIMEOUT_MS — the one value in this migration that
is deliberately tiny (20ms), used to force a timeout in a negative test,
not generous headroom.

No src/bin file touched, no numeric value changed anywhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 19:22:04 -04:00
Tom Boucher
b8c70a33c0 fix(#4454): surface skipped explicit --files paths instead of silent success (#4538)
* fix(#4454): surface skipped explicit --files paths instead of silent success

cmdCommit's staging loop deliberately skips a --files path that no longer
exists on disk when the caller passed --files explicitly (#2014 guards
against staging an unwanted deletion for a temporarily-absent file), but
recorded nothing about the skip. The final result reported unqualified
committed: true, so a caller had no way to distinguish "everything in
--files landed" from "some paths were silently excluded because they
didn't exist" -- a real file deletion meant to be committed (e.g.
phase.complete removing .planning/milestone.lock) would silently never
land, with git status still showing it unstaged after a "successful"
commit.

Tracks skipped paths in a skippedFiles array and surfaces them as
skipped_files (matching this result family's existing snake_case
precedent, timed_out) in the success result AND the nothing_to_commit
result (reachable when every named path was missing), included only
when non-empty so the common case's payload shape is unchanged. Also
extended to the SECOND nothing_to_commit result (reached when git
itself reports "nothing to commit" after the nothingToCommit guard was
false -- the residual partial-skip + partialCommitRefused window the
surrounding comments already document) for the same consistency. The
#2014 staging/deletion guard itself is untouched -- purely a visibility
fix, exactly as the issue requested.

Regression tests cover: existing-plus-missing file (the issue's own
repro shape, with the missing path genuinely tracked-then-deleted so
the #2014 assertion is meaningful, not vacuous against an
never-tracked path); only-existing files (no skipped_files key at
all); only-missing files (nothing_to_commit with skipped_files);
default mode unaffected; and multiple missing files reported in order.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: recover ack trailers buried by squash-merge concatenation

Discovered while verifying an unrelated PR (#4454): tests/emitted-attribution.test.cjs
failed citing pr-branch.md's growth as unacknowledged, even though PR #4537
(#4447) had genuinely acked it via an Emitted-Drift-Ack-Growth commit trailer.

Root cause: git's %(trailers:...) placeholder (used by readAckTrailers) only
recognizes a trailer block that is the TRUE TERMINAL block of a commit
message. GitHub's squash-merge commit body concatenates every constituent
commit's subject+body in order, then unconditionally appends its own
`---------` separator and Co-authored-by trailers. Confirmed directly
against a real squash commit: %(trailers) returns ONLY GitHub's own two
Co-authored-by lines -- not even the LAST original commit's own trailer
survives, since GitHub's appended suffix breaks the backward scan before
it ever reaches past `---------` to any original commit's content, last
or not. An ack trailer added on any non-final commit of a PR (the common
case -- more commits routinely land after an ack, e.g. a lint fix or a
changeset) is therefore silently invisible forever to any future
comparison against a base predating that squash.

Fix: readAckTrailers now runs a second pass (extractSquashBuriedTrailers)
alongside the existing whole-message read. For each commit in range, if
its raw body contains GitHub's squash-suffix signal, the pre-suffix text
(everything before the LAST such signal -- an earlier bullet's own body
may legitimately contain a markdown horizontal rule that looks the same,
so anchoring on the first occurrence would truncate too early and miss a
later bullet's real ack) is split on squash-bullet (`* <subject>`)
boundaries, and each chunk is independently trailer-parsed via
`git interpret-trailers --parse` -- the same underlying algorithm as
%(trailers:...), but runnable against arbitrary text rather than only a
real commit object. This finds a trailer buried in ANY bullet, not just
the last one.

Scoped tightly to avoid reintroducing the false-positive class
%(trailers:...) was originally chosen to prevent (a mid-body MENTION of
trailer syntax must stay inert): the sub-chunk pass activates only on
commits matching the squash-suffix signal, so an ordinary commit whose
body happens to contain markdown bullets is completely unaffected, and
each chunk still goes through git's own strict per-chunk terminal-block
detection.

Verified end-to-end against a real squash commit (recovers the buried
trailer) and four adversarial fixtures now pinned as regression tests:
ordinary bullet prose with no squash suffix (stays empty); a
squash-shaped commit where one bullet's body merely mentions trailer
syntax mid-paragraph (stays inert, the "row 32" false-positive class,
now verified at per-chunk granularity); and an earlier bullet's own
markdown horizontal rule not truncating the scan before a later bullet's
real ack (the last-match-not-first-match case an isolated review pass
caught during this same fix).

This overrides one-concern-per-PR per CLAUDE.md's Defects & Warnings
policy -- a genuine defect discovered mid-work is fixed inline, not
deferred to a separate issue (spawn_task for this was correctly blocked
by the no-defer guard).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(#4454): backfill changeset PR number

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 02:01:12 -04:00
Tom Boucher
cf3eb84b3f fix(tests): stop racing spawnSync's documented timeout boundary in row 24 (#3988)
emitted-ack-trailer.test.cjs's "git log is bounded by a timeout" row asserted
readAckTrailers throws on timeoutMs: 1 against a real git call. Empirically
confirmed (5 direct probes against the exact gsd-tester image gsd-test uses)
that a trivial git merge-base can complete inside that 1ms window ~1-in-5
runs, landing on process-seam.cjs's own documented "child finished right at
the deadline" boundary case (status populated alongside error.code ===
ETIMEDOUT) -- which the seam correctly classifies as EXITED, not TIMED_OUT.
readAckTrailers, ackTrailerGit, and process-seam.cjs are all already correct;
only the test's mechanism for forcing a timeout was racy.

Replaces the real-timing race with a deterministic PATH-shim `git` that
idles via the same non-blocking setTimeout sleeper pattern already
established in tests/process-seam.test.cjs, given a 100x margin (5000ms
sleep vs 50ms bound) so the race is eliminated by construction.

Discovered while verifying an unrelated docs-only PR (#3969 / ADR-3646 for
#3646) -- this test's failure blocked gsd-test's push gate for every branch
cut from next, independent of diff content.

Closes #3985

Co-authored-by: sim <sim@local>
2026-08-28 09:18:57 -04:00
Tom Boucher
fa41bfec5c enhance(#3942): the emitted-drift ack is PR-lifetime data — move it to a commit trailer (#3954)
* test(#3942): failing-first suite for the emitted-drift ack commit trailer

Binds 37 input classes from the phase test matrix to the behavior ADR-3942
specifies, before any of it exists. Stubs return benign empty values rather
than throwing, deliberately: several rows assert that something DOES throw
(cap overflow, uncomputable commit range), and a throwing stub would turn
those green for the wrong reason and destroy the red.

The two rows that carry the design's load:

- merge-base semantics. The range is $(git merge-base base HEAD)..HEAD, not
  base..HEAD, because changedPaths comes from `git diff base...HEAD` (three
  dot). Two-dot would let the ack set and the change set disagree about which
  commits are this PR's. The fixture forks a topic branch, puts a trailer on
  each side, and asserts only the topic-side trailer is in range.

- fail-closed on an uncomputable range. With fragments a depth-1 checkout
  passes VACUOUSLY, every fragment reading as brand-new. With trailers the
  range cannot be computed at all, and returning an empty set would silently
  disarm the gate, so it must throw. The fixture builds a genuine shallow
  clone rather than simulating one.

Also covers the self-inflicted case: this change's own documentation quotes
the trailer syntax, so an example landing at the end of a commit message would
arm a live acknowledgment keyed on the literal placeholder text. Keys carrying
angle brackets or whitespace are rejected.

Authored per the phase artifacts 40-design.md and 50-test-matrix.md.
Not yet run on the remote runner — this commit exists to be tested.

Refs #3942

* chore(#3942): move the emitted-drift ack to a commit trailer

Implements ADR-3942, superseding ADR-2719 section 3 and its #2789 amendment.
Sections 1, 2 and 4-7 are retained: the conservation law is unchanged, only the
storage of its escape hatch moved off the working tree.

An acknowledgment explains one PR's ripple, and the moment that PR merges the
ripple is in the base, so it can never clear anything again. It was stored in
permanent shared state anyway, and every consequence of that mismatch had to be
built and then maintained. The chain is #2789 -> #2914 -> #3078 -> #3842 ->
#3823 -> #3875, each fix generating the next defect, ending in a scheduled
sweeper whose own first PR could not merge itself.

Added
  parseAckTrailers + renderAckTrailer (pure) and readAckTrailers (IO shell),
  reading Emitted-Drift-Ack-Hash: / Emitted-Drift-Ack-Growth: trailers over
  the merge-base range. tests/emitted-ack-trailer.test.cjs, 37 cases, written
  failing-first and confirmed red before any of this existed.

Changed
  diffEmitted takes two structurally distinct key-space maps instead of one
  shared paths map. That closes a latent defect: the spaces were separated by
  convention only, so a growth key satisfied a hash lookup by naming
  coincidence. staleAcks now reports which space a key was declared in.
  REMEDIATION teaches the trailer, per space, with its example rendered through
  renderAckTrailer so the taught grammar cannot drift from what the parser
  accepts.

Removed
  the sweep workflow, the guard-no-ack-on-next job, the standalone linter and
  its lint:ci entry, the fragment directory and its three spent fragments, the
  legacy single-file union, and the baseAck/spentAcks mechanism -- spentness is
  now structural, not computed.

Two range properties carry the design and are pinned by tests rather than
asserted: the range is merge-base scoped, matching git diff base...HEAD, so an
already-merged trailer is out of range by construction; and an uncomputable
range throws instead of reading as zero acknowledgments, which is the inverse
of the fragment guard's vacuous pass.

Three deliberate observable changes, each disclosed in the changeset: the
unread runtime field is gone, the legacy file is no longer read, and cross-space
excusal no longer works.

Ten open PRs carry fragments and will meet a modify/delete conflict. Measured
before landing and accepted deliberately; the one-line migration is in the PR
body.

Verified: lint:ci exit 0. Remote runner to follow on this exact sha.

Refs #3942

* fix(#3942): silent trailer collapse, lost coverage, and an unbounded cap

Six findings from the orthogonal review round, all fixed in place.

BLOCKER -- two trailers of the same name on one commit collapsed silently.
readAckTrailers built `separator=1d` where git needs `separator=%x1d`: the
`separator=` value inside a %(trailers:...) placeholder is itself a
pretty-format string, so the bare hex was emitted as two literal characters
and the split on \x1d never matched. Two same-name trailers therefore joined
into one value with errors empty -- the first reason absorbing the second
entry's key. Silent truncation, the exact class MAX_ACK_TRAILERS throws to
prevent. Confirmed with od -c against real git output before and after.

The failing-first matrix did not catch it because its "both spaces coexist"
row uses Hash plus Growth -- different trailer NAMES -- so the value separator
was never exercised. Two regression tests now cover same-name trailers
directly.

Coverage recovered: normalizeAckReason and INVISIBLE stayed on the live path
via parseAckTrailers but lost every test when the old suite was pruned. Back
under test against the current surface -- all six invisible codepoints
individually, whitespace collapse, trim, CRLF, and two seeded fast-check
properties. Dropping any single codepoint now fails.

MAX_ACK_TRAILERS counted raw trailers before de-duplication, so one trailer
carried forward across rebased commits counted once per commit and could throw
on a legitimate branch. Now counts distinct entries; 100 identical repeats
dedupe to one.

diffEmitted validated baseline, current and changedPaths but not the new
ackHash/ackGrowth, so a bad shape raised an unhandled TypeError instead of an
error verdict -- the same defect shape this file documents for #2778.

Docs: CONTRIBUTING and TESTING-SUITES were rewritten only in their first
sections; the later passages still taught fragments, git rm and the deleted
guard, contradicting the new text directly above them. Finished.

Also extends lint-removed-but-needed to exempt docs/adr and docs/research.
That gate fails on any docs mention of a file deleted in the same diff, which
makes it impossible to document a deletion in the PR performing it -- an ADR's
whole job is naming what it retired. Exemption is narrow and comes with a test
proving the gate still fires for a live consumer elsewhere under docs/. A
guard that cannot fail is worse than no guard. Maintainer-approved.

CONTEXT.md names the retired machinery by role rather than by filename: its
generated projection lands in docs/, which that gate does scan.

Adds docs/how-to/acknowledge-emitted-drift.md. The required docs set is
Reference and Explanation, so the task quadrant can be empty with every gate
green -- and this change has a real multi-step journey, including the fragment
migration ten open PRs now need.

lint:ci exit 0.

Refs #3942

* docs(#3942): correct the duplicate-trailer rule in CONTRIBUTING

Both axes of the code review independently flagged the same passage, without
seeing each other's output.

It claimed two declarations of the same key are always "a hard, loudly-reported
error, not a silent last-wins". That is only half true, and the missing half is
the one contributors hit: identical declarations -- same key, same reason --
dedupe silently, because a trailer legitimately survives a rebase and reappears
on every rebased commit. Failing there would red a branch for doing nothing
wrong, which is exactly why the dedup exists.

Only a same-key/different-reason pair errors, and that one is a genuine
ambiguity about which explanation holds.

As written, the paragraph told a contributor that a rebase-carried trailer
breaks the gate -- the opposite of the behavior. CONTEXT.md's parallel entry
already stated it correctly; this brings CONTRIBUTING into line.

Doc-only, root-level markdown.

Refs #3942

* chore(#3942): backfill changeset PR number to 3954

---------

Co-authored-by: sim <sim@local>
2026-08-27 17:28:39 -04:00