Files
msd-core/docs/how-to/resolve-contract-drift-findings.md
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

6.8 KiB

How to resolve a contract-drift finding

Goal: Bring msd-core/references/agent-contracts.md's Agent Registry back into agreement with what agents actually emit and what workflows, commands, and agents actually consume — so a completion marker, a read-tag gate, or a deleted-file reference can never silently drift apart again.

Prerequisites: A failing npm run lint:ci (or a direct npm run check:contract-drift) reporting one or more violations. The check runs automatically as part of lint:ci — you do not invoke it separately.

For why the registry exists (five shipped defects, all one root cause: two surfaces sharing a contract with nothing enforcing agreement), see issue #3565 and epic #1891. This guide covers only how to act on a finding.


Read a finding

ERROR check-contract-drift: 2 violation(s) across 2 kind(s)

  no_consumer (1):
    - msd-example marker "EXAMPLE COMPLETE": no consumer text contains "EXAMPLE COMPLETE" (exact case)
      remedy: add an exact-case consumer for this marker, or reclassify the row Kind to artifact+query/structured-return

Every finding names its kind, the agent (or registry), the marker where one is involved, and a remedy line. The kinds and what they mean:

Kind Meaning Correct resolution
no_consumer A declared sentinel-match marker no workflow/command/agent matches exact-case Wire the consumer, fix the casing on either side, or reclassify the row's Kind
declared_consumer_no_match The marker is consumed somewhere, but by none of the files the row's Consumed by cell names Fix the cell to name a real consumer — the cell is what the read-tag arm and humans navigate by
case_only_match A marker matches a consumer only case-insensitively Fix the casing to match exactly — never relax the matcher; lowercase prose is a coincidence, not a contract
case_collision Two declared markers differ only by case Rename one deliberately; a case-insensitive runtime match cannot tell them apart
declared_marker_not_emitted The registry declares a marker the agent never emits in-fence Emit it as a fenced example in the agent file (a @-included msd-core/references/** file counts), or remove it from the row
emitted_marker_not_declared The agent emits an in-fence marker-shaped heading absent from its row Add it to the row — after verifying a real consumer exists (it may be your next no_consumer)
vestigial_marker An artifact+query/structured-return row's agent still emits a marker Delete the marker from the agent — unless it carries an (unconsumed: …) annotation (see below)
agent_without_contract / duplicate_registry_row An agent file with no row, or two rows Add exactly one row; the registry is the declaration of record for every agent
unknown_producer / unknown_consumer A row names an agent or a Consumed by file that does not exist Fix or drop the name — a lint over fictional paths guards nothing
read_tag_gate_missing A declared consumer emits <required_reading> but the agent never references the gate Add the MUST-Read gate clause to the agent (an @-included reference carrying it counts) — this is the F8 defect one layer up
legacy_read_tag A <files_to_read> survived under workflows/commands/agents Rename to <required_reading> (#3423 standardized on the gate tag)
unmatched_consumer_token A workflow/command matches a quoted ## TOKEN no agent declares or emits Declare the producer, or delete the dispatch — this is F9's shape from the consumer side
parse_error / unclosed_fence The registry table or an agent file is malformed Fix the row / close the fence; extraction over an unclosed fence is unreliable
pins-existence (from lint-removed-but-needed) A test depends on a file this PR deleted Update the test to assert absence instead — assert.ok(!content.includes('x.md')) is the correct post-deletion state

Pick the right Kind

Every row declares exactly one of three kinds, and the kind is a measured fact about the consumer, not a style preference:

  • sentinel-match — a workflow, command, or another agent detects completion by an exact-case string match. Only kind whose markers need a consumer.
  • artifact+query — the agent writes a file and the caller reads or queries it (*-VERIFICATION.md + msd_run query verification.status). No marker required.
  • structured-return — the agent cannot write (no Write tool) or returns parseable sections/JSON inline, and the caller reads the return text.

To decide, open the consumer and find the line that waits for the agent. If it greps a marker string → sentinel-match. If it reads a file or queries a CLI → artifact+query. If it parses the return message → structured-return.

The (unconsumed: …) annotation — an audit trail, not a mute button

Some markers are emitted deliberately while nothing matches them: Marker Rule 2's title-case markers are a recorded decision, and a draft-presentation heading may serve a human, not a spawner. Declare those as:

`## ROADMAP DRAFT` (unconsumed: draft-presentation format the shipped execution flow never invokes — Step 8 returns `## ROADMAP CREATED`)

The annotation waives only the consumer requirement. The marker must still be declared and emitted, and it still counts for case-collision detection. A non-empty reason is required — the reason is what makes the exemption auditable rather than a silent pass. Never use it to silence a real orphan; if no reason survives scrutiny, the marker is dead and should be deleted.

After editing the registry

Nothing regenerates from agent-contracts.md — it is the source of truth, read directly by the check. Re-run:

npm run check:contract-drift

If the failing finding came from the tests/ arm of lint-removed-but-needed, re-run npm run lint:removed-but-needed with MSD_REMOVED_BUT_NEEDED_BASE pointing at your base branch (CI sets this automatically).

Silent cases are clean by design

  • A marker heading outside a code fence is prose documentation, not a contract — the check ignores it (fence-awareness is what keeps 7 legitimate prose headings from being reported as case-drift).
  • An (unconsumed: …) marker with no consumer is not an orphan — that is the annotation working.
  • A test that asserts absence of a deleted file is not a surviving reference — that is the discriminator working.
  • Nothing to report and nothing to fix look identical in the output: ok check:contract-drift: N agents, M markers, 0 violations. The agent/marker counts moving between runs is your signal the registry actually changed.