Add the unchanged gate for structure-only changes #1106

Closed
opened 2026-10-18 16:24:07 +00:00 by jared · 2 comments
Owner

Problem

A structure-only change (rename, move, split) needs proof that behavior did not change.
No os-sdlc gate compares the merge-base with HEAD, so none can give that proof.
The existing suite is the proof only if the run count and the lint offense count stay equal.

Reproduce

n/a: new gate; nothing exists to reproduce.

Approach

Add Gate::Name::UNCHANGED and bin/os-sdlc-unchanged-check IMPL_ID.
The check runs the target suite command and rubocop --format json once at the merge-base, in a detached temporary worktree, and once at HEAD.
It parses the suite line with Suite::Output::Summary.
It passes when HEAD has 0 failures and 0 errors, HEAD runs equals base runs minus the runs of tests listed under ## Retired tests, and the whole-target offense count equals the base count.
The offense count covers the whole target, not only the diffed files, so it catches broken path globs.
The gate name is parsed vocabulary (maps parse it). Run /os-adr:find first, and run /os-adr:create if it reports a one-way door.

Required work:

  1. Add the gate name — check: present UNCHANGED in plugins/os-sdlc/lib/os_sdlc/gate/name.rb
  2. Add the check CLI — check: present unchanged in plugins/os-sdlc/bin/os-sdlc-unchanged-check

Orchestrator work: run /os-adr:find for the gate name, then /os-adr:create if it reports a one-way door.

Acceptance criteria

  1. Given a diff that only moves a file and its mirrored test, when os-sdlc-unchanged-check IMPL_ID runs, then it passes.
  2. Given a diff that deletes a test not listed under ## Retired tests, when the check runs, then it fails and reports the base and HEAD run counts.
  3. Given a diff that deletes a test listed under ## Retired tests, when the check runs, then HEAD runs equal base runs minus the runs of that test and the run count check passes.
  4. Given a diff that breaks a .rubocop.yml Exclude glob so the whole-target offense count rises, when the check runs, then it fails and reports both counts.
  5. Given HEAD with one suite failure, when the check runs, then it fails.

Owner sketch

class OsSdlc::Gate::Unchanged
  def call(implementation)
end

Origin

  • Trigger: scope memo on why CRC and collaborator-pass structure tickets misroute (structure-ticket-route-scope.md, 2026-10-18)
  • Improvised this session: none
  • Chain: structure tickets have no deterministic proof of "unchanged" ← no gate compares base and HEAD ← DESIGN (gates check only HEAD)
  • Root candidate: none
  • Where: plugins/os-sdlc/lib/os_sdlc/gate/name.rb
  • Session: 26783407-6911-48d5-a818-55fbfbc70108
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/26783407-6911-48d5-a818-55fbfbc70108.jsonl

VERDICT: CREATE

## Problem A structure-only change (rename, move, split) needs proof that behavior did not change. No os-sdlc gate compares the merge-base with HEAD, so none can give that proof. The existing suite is the proof only if the run count and the lint offense count stay equal. ## Reproduce n/a: new gate; nothing exists to reproduce. ## Approach Add `Gate::Name::UNCHANGED` and `bin/os-sdlc-unchanged-check IMPL_ID`. The check runs the target suite command and `rubocop --format json` once at the merge-base, in a detached temporary worktree, and once at HEAD. It parses the suite line with `Suite::Output::Summary`. It passes when HEAD has 0 failures and 0 errors, HEAD `runs` equals base `runs` minus the runs of tests listed under `## Retired tests`, and the whole-target offense count equals the base count. The offense count covers the whole target, not only the diffed files, so it catches broken path globs. The gate name is parsed vocabulary (maps parse it). Run `/os-adr:find` first, and run `/os-adr:create` if it reports a one-way door. Required work: 1. Add the gate name — check: present `UNCHANGED` in `plugins/os-sdlc/lib/os_sdlc/gate/name.rb` 2. Add the check CLI — check: present `unchanged` in `plugins/os-sdlc/bin/os-sdlc-unchanged-check` Orchestrator work: run `/os-adr:find` for the gate name, then `/os-adr:create` if it reports a one-way door. ## Acceptance criteria 1. Given a diff that only moves a file and its mirrored test, when `os-sdlc-unchanged-check IMPL_ID` runs, then it passes. 2. Given a diff that deletes a test not listed under `## Retired tests`, when the check runs, then it fails and reports the base and HEAD run counts. 3. Given a diff that deletes a test listed under `## Retired tests`, when the check runs, then HEAD runs equal base runs minus the runs of that test and the run count check passes. 4. Given a diff that breaks a `.rubocop.yml` Exclude glob so the whole-target offense count rises, when the check runs, then it fails and reports both counts. 5. Given HEAD with one suite failure, when the check runs, then it fails. ## Owner sketch ```ruby class OsSdlc::Gate::Unchanged def call(implementation) end ``` ## Origin - Trigger: scope memo on why CRC and collaborator-pass structure tickets misroute (structure-ticket-route-scope.md, 2026-10-18) - Improvised this session: none - Chain: structure tickets have no deterministic proof of "unchanged" ← no gate compares base and HEAD ← DESIGN (gates check only HEAD) - Root candidate: none - Where: plugins/os-sdlc/lib/os_sdlc/gate/name.rb - Session: 26783407-6911-48d5-a818-55fbfbc70108 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/26783407-6911-48d5-a818-55fbfbc70108.jsonl VERDICT: CREATE
Author
Owner

Work started on branch ticket-1106 via /os-sdlc:implement.

Work started on branch ticket-1106 via /os-sdlc:implement.
Author
Owner

Resolution

Done: Gate::Name::UNCHANGED plus bin/os-sdlc-unchanged-check IMPL_ID: measures the target suite and rubocop offense count at the merge-base (detached temporary checkout) and at HEAD; passes when HEAD has 0 failures and 0 errors, HEAD runs equal base runs minus retired tests, and the whole-target offense count equals base. Gate name registered in Gate::Commands and Boot::Commands.

Evidence: ticket-1106 branch 59a939c7 (Runner commit), 12 files, 371 insertions; pipeline run 66: 10 dispatches, 0 retries, 13 min 38 s; suite gate pass; contract-auditor pass; adr-find returned no candidates for the gate name (additive vocabulary, no ADR).

Follow-ups: #1107 consumes this gate in the structure-change map. Runner commit subject was built from the contract-auditor summary rather than the change; captured as a separate ticket this session.

## Resolution **Done:** Gate::Name::UNCHANGED plus bin/os-sdlc-unchanged-check IMPL_ID: measures the target suite and rubocop offense count at the merge-base (detached temporary checkout) and at HEAD; passes when HEAD has 0 failures and 0 errors, HEAD runs equal base runs minus retired tests, and the whole-target offense count equals base. Gate name registered in Gate::Commands and Boot::Commands. **Evidence:** ticket-1106 branch 59a939c7 (Runner commit), 12 files, 371 insertions; pipeline run 66: 10 dispatches, 0 retries, 13 min 38 s; suite gate pass; contract-auditor pass; adr-find returned no candidates for the gate name (additive vocabulary, no ADR). **Follow-ups:** #1107 consumes this gate in the structure-change map. Runner commit subject was built from the contract-auditor summary rather than the change; captured as a separate ticket this session.
jared closed this issue 2026-10-18 17:07:45 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#1106
No description provided.