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.
4.4 KiB
id, title, group
| id | title | group |
|---|---|---|
| 3912 | msd-tools Declares Outcomes, Pinned at v1 | v1.7.0 Features |
Purpose: Give every msd-tools terminating path a declared outcome name, and project that
declaration through the versioned exit contract (ADR-3889
§4) — without changing a single exit code for a caller that has not opted in.
Reference — what changed (ADR-3889 Phase 8, #3912):
error(message, reason)now maps itsreasonargument onto a declared outcome name (USAGE,NO_INPUT,UNAVAILABLE,INTERNAL,FAIL) via a fixed table closed over all 25ERROR_REASONmembers (src/io.cts'sREASON_TO_OUTCOME). Under the default contract versionv1, the declaration is recorded buterror()still throwsExitError(1)unconditionally, byte-identical to every prior release. Underv2(--exit-contract=v2/MSD_EXIT_CONTRACT=v2), it throwsExitError(projectOutcome(outcome, 'v2'))instead — e.g.SDK_MISSING_ARG/SDK_UNKNOWN_COMMANDproject to64(USAGE),CONFIG_KEY_NOT_FOUNDto66(NO_INPUT). All 278 call sites are untouched; 226 pass no reason and default toUNKNOWN->FAIL-> exit1under both versions.output()now declaresDEGRADEDwhenever its payload carries a serializableerrorvalue (any key order —{found:false, error}counts the same as{error, found:false}). The discriminator is survives-JSON.stringify, not mere key presence:{ error: undefined }does not declareDEGRADED, becauseJSON.stringifydrops anundefined-valued property before the payload reaches the wire.- A third
globalThiscell (src/cli-exit.cts'sPENDING_OUTCOME_KEY) holds the pending declared outcome betweenoutput()andrunMain. Semantics: last declaration wins, cleared on consumption — a later cleanoutput()call in the same invocation undoes an earlier degraded one, andrunMainclears the cell on every exit so a secondrunMainin the same process never inherits a stale declaration. - Precedence for the code a void-returning
main()ends up with, highest first: (1) an explicitmain()return, (2) a non-zeroprocess.exitCodemain()already set directly, (3) the pending declared outcome, (4) otherwise0. Projection may only ever set a code, never lower one — a review pass wrongly concluded the cell was fail-closed by construction; without rule 2,state validate --strictbriefly exited0where it must exit1. v1is byte-identical.DEGRADEDprojects to0underv1and to80(exitCodeFor('DEGRADED')) underv2— that asymmetry is ADR-2980's compatibility boundary, deliberately preserved, not a bug to reconcile.
Explanation — why this is the shape it is:
ADR-2980 ratified output({error})'s exit-0 population on measured blast radius (output has 170
direct callers) and Hyrum's Law grounds — a CLI exit code has no /v2/ of its own, so normalizing
it in place would have broken every caller already treating exit 0 as a soft signal. Its own
"Revisit if" clause named the missing piece: "a future msd-tools major version provides a
compatibility boundary that a CLI exit code otherwise lacks." ADR-3889 §4 built exactly that
boundary — a versioned projection selected by flag or env var, defaulting to today's behavior — and
this phase is what wires error() and output() onto it. Declaring an outcome is unconditional and
immediate; only its projection onto an integer is deferred behind the version switch, so the
population ADR-2980 ratified keeps exiting 0 until a caller explicitly asks for something else.
The count matters here too: an AST re-measure for this phase found 64 output({error}) call
sites across the same nine modules ADR-2980 named — not the 60 that ADR itself recorded, the drift
concentrated in frontmatter.cts, phase.cts, and roadmap.cts. The v2 projection is asserted
over the enumerated 64, not a restated 60; see ADR-2980's amendment for the module-by-module
breakdown.
See Adopt the v2 exit contract for how to opt in and what
it means for a CI gate, docs/json-errors.md
for the full reference, and
ADR-2980 /
ADR-3889 for the decisions.