git 5.0.0.beta.2 → 5.0.0.beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. checksums.yaml +4 -4
  2. data/.dockerignore +27 -0
  3. data/.github/copilot-instructions.md +3 -8
  4. data/.github/hooks/bin-setup-on-worktree.json +11 -0
  5. data/.github/hooks/run-bin-setup-once.sh +14 -0
  6. data/.github/skills/breaking-change-analysis/SKILL.md +6 -9
  7. data/.github/skills/ci-cd-troubleshooting/SKILL.md +2 -2
  8. data/.github/skills/command-implementation/REFERENCE.md +34 -33
  9. data/.github/skills/command-implementation/SKILL.md +5 -4
  10. data/.github/skills/command-test-conventions/SKILL.md +13 -9
  11. data/.github/skills/command-yard-documentation/SKILL.md +14 -6
  12. data/.github/skills/development-workflow/SKILL.md +20 -14
  13. data/.github/skills/facade-implementation/REFERENCE.md +19 -22
  14. data/.github/skills/facade-implementation/SKILL.md +0 -5
  15. data/.github/skills/facade-test-conventions/SKILL.md +4 -7
  16. data/.github/skills/facade-yard-documentation/SKILL.md +8 -2
  17. data/.github/skills/make-skill-template/SKILL.md +53 -3
  18. data/.github/skills/pr-readiness-review/SKILL.md +22 -2
  19. data/.github/skills/project-context/SKILL.md +16 -23
  20. data/.github/skills/pull-request-review/SKILL.md +2 -2
  21. data/.github/skills/rebase/SKILL.md +148 -0
  22. data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +1 -1
  23. data/.github/skills/resolve-feedback/SKILL.md +288 -0
  24. data/.github/skills/review-arguments-dsl/CHECKLIST.md +3 -3
  25. data/.github/skills/reviewing-skills/SKILL.md +28 -3
  26. data/.github/skills/test-debugging/SKILL.md +20 -19
  27. data/.github/skills/yard-documentation/SKILL.md +671 -483
  28. data/.github/skills/yard-documentation/element-rules.md +162 -0
  29. data/.github/skills-deprecated/README.md +21 -0
  30. data/.github/{skills → skills-deprecated}/extract-command-from-lib/SKILL.md +9 -9
  31. data/.github/{skills → skills-deprecated}/extract-facade-from-base-lib/SKILL.md +12 -12
  32. data/.github/workflows/continuous_integration.yml +3 -2
  33. data/.rubocop.yml +7 -39
  34. data/.yard-lint.yml +75 -0
  35. data/CONTRIBUTING.md +186 -174
  36. data/Gemfile +17 -0
  37. data/README.md +86 -23
  38. data/Rakefile +3 -21
  39. data/UPGRADING.md +219 -70
  40. data/docker/test/Dockerfile +32 -0
  41. data/docker/test/docker-compose.yml +0 -0
  42. data/git.gemspec +3 -4
  43. data/lib/git/author.rb +20 -1
  44. data/lib/git/branch.rb +26 -6
  45. data/lib/git/branch_delete_failure.rb +3 -0
  46. data/lib/git/branch_delete_result.rb +6 -3
  47. data/lib/git/branch_info.rb +6 -6
  48. data/lib/git/command_line/base.rb +6 -13
  49. data/lib/git/command_line/capturing.rb +67 -10
  50. data/lib/git/command_line/result.rb +0 -8
  51. data/lib/git/command_line/streaming.rb +45 -5
  52. data/lib/git/command_line.rb +4 -3
  53. data/lib/git/commands/add.rb +61 -53
  54. data/lib/git/commands/am/abort.rb +2 -2
  55. data/lib/git/commands/am/apply.rb +109 -98
  56. data/lib/git/commands/am/continue.rb +2 -2
  57. data/lib/git/commands/am/quit.rb +2 -2
  58. data/lib/git/commands/am/retry.rb +4 -2
  59. data/lib/git/commands/am/show_current_patch.rb +1 -1
  60. data/lib/git/commands/am/skip.rb +2 -2
  61. data/lib/git/commands/am.rb +2 -2
  62. data/lib/git/commands/apply.rb +7 -2
  63. data/lib/git/commands/archive/list_formats.rb +3 -3
  64. data/lib/git/commands/archive.rb +7 -2
  65. data/lib/git/commands/arguments.rb +1370 -359
  66. data/lib/git/commands/base.rb +111 -13
  67. data/lib/git/commands/branch/copy.rb +29 -21
  68. data/lib/git/commands/branch/create.rb +72 -68
  69. data/lib/git/commands/branch/delete.rb +25 -17
  70. data/lib/git/commands/branch/list.rb +76 -60
  71. data/lib/git/commands/branch/move.rb +29 -21
  72. data/lib/git/commands/branch/set_upstream.rb +8 -8
  73. data/lib/git/commands/branch/show_current.rb +3 -3
  74. data/lib/git/commands/branch/unset_upstream.rb +4 -8
  75. data/lib/git/commands/branch.rb +2 -2
  76. data/lib/git/commands/cat_file/batch.rb +36 -20
  77. data/lib/git/commands/cat_file/filtered.rb +12 -12
  78. data/lib/git/commands/cat_file/raw.rb +13 -13
  79. data/lib/git/commands/cat_file.rb +4 -4
  80. data/lib/git/commands/checkout/branch.rb +7 -5
  81. data/lib/git/commands/checkout/files.rb +5 -4
  82. data/lib/git/commands/checkout.rb +4 -4
  83. data/lib/git/commands/checkout_index.rb +5 -4
  84. data/lib/git/commands/clean.rb +6 -4
  85. data/lib/git/commands/clone.rb +5 -4
  86. data/lib/git/commands/commit.rb +5 -4
  87. data/lib/git/commands/commit_tree.rb +5 -4
  88. data/lib/git/commands/config_option_syntax/add.rb +7 -4
  89. data/lib/git/commands/config_option_syntax/get.rb +9 -5
  90. data/lib/git/commands/config_option_syntax/get_all.rb +7 -4
  91. data/lib/git/commands/config_option_syntax/get_color.rb +8 -4
  92. data/lib/git/commands/config_option_syntax/get_color_bool.rb +7 -4
  93. data/lib/git/commands/config_option_syntax/get_regexp.rb +8 -4
  94. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +8 -4
  95. data/lib/git/commands/config_option_syntax/list.rb +8 -4
  96. data/lib/git/commands/config_option_syntax/remove_section.rb +7 -2
  97. data/lib/git/commands/config_option_syntax/rename_section.rb +7 -2
  98. data/lib/git/commands/config_option_syntax/replace_all.rb +7 -2
  99. data/lib/git/commands/config_option_syntax/set.rb +7 -2
  100. data/lib/git/commands/config_option_syntax/unset.rb +7 -4
  101. data/lib/git/commands/config_option_syntax/unset_all.rb +7 -2
  102. data/lib/git/commands/describe.rb +5 -4
  103. data/lib/git/commands/diff.rb +13 -12
  104. data/lib/git/commands/diff_files.rb +7 -6
  105. data/lib/git/commands/diff_index.rb +10 -7
  106. data/lib/git/commands/fetch.rb +6 -4
  107. data/lib/git/commands/fsck.rb +6 -4
  108. data/lib/git/commands/gc.rb +6 -4
  109. data/lib/git/commands/grep.rb +6 -5
  110. data/lib/git/commands/init.rb +6 -4
  111. data/lib/git/commands/log.rb +287 -285
  112. data/lib/git/commands/ls_files.rb +69 -65
  113. data/lib/git/commands/ls_remote.rb +62 -56
  114. data/lib/git/commands/ls_tree.rb +50 -46
  115. data/lib/git/commands/maintenance/register.rb +25 -23
  116. data/lib/git/commands/maintenance/run.rb +7 -2
  117. data/lib/git/commands/maintenance/start.rb +7 -2
  118. data/lib/git/commands/maintenance/stop.rb +7 -2
  119. data/lib/git/commands/maintenance/unregister.rb +7 -2
  120. data/lib/git/commands/merge/abort.rb +2 -2
  121. data/lib/git/commands/merge/continue.rb +2 -2
  122. data/lib/git/commands/merge/quit.rb +2 -2
  123. data/lib/git/commands/merge/start.rb +7 -2
  124. data/lib/git/commands/merge.rb +2 -2
  125. data/lib/git/commands/merge_base.rb +7 -2
  126. data/lib/git/commands/mv.rb +7 -2
  127. data/lib/git/commands/name_rev.rb +7 -2
  128. data/lib/git/commands/pull.rb +7 -2
  129. data/lib/git/commands/push.rb +7 -2
  130. data/lib/git/commands/read_tree.rb +7 -2
  131. data/lib/git/commands/remote/add.rb +7 -2
  132. data/lib/git/commands/remote/get_url.rb +4 -2
  133. data/lib/git/commands/remote/list.rb +4 -2
  134. data/lib/git/commands/remote/prune.rb +4 -2
  135. data/lib/git/commands/remote/remove.rb +4 -2
  136. data/lib/git/commands/remote/rename.rb +4 -2
  137. data/lib/git/commands/remote/set_branches.rb +9 -2
  138. data/lib/git/commands/remote/set_head.rb +12 -5
  139. data/lib/git/commands/remote/set_url.rb +9 -2
  140. data/lib/git/commands/remote/set_url_add.rb +11 -2
  141. data/lib/git/commands/remote/set_url_delete.rb +9 -2
  142. data/lib/git/commands/remote/show.rb +18 -12
  143. data/lib/git/commands/remote/update.rb +19 -12
  144. data/lib/git/commands/remote.rb +2 -2
  145. data/lib/git/commands/repack.rb +67 -63
  146. data/lib/git/commands/reset.rb +51 -44
  147. data/lib/git/commands/rev_parse.rb +135 -128
  148. data/lib/git/commands/revert/abort.rb +2 -2
  149. data/lib/git/commands/revert/continue.rb +7 -2
  150. data/lib/git/commands/revert/quit.rb +2 -2
  151. data/lib/git/commands/revert/skip.rb +2 -2
  152. data/lib/git/commands/revert/start.rb +7 -2
  153. data/lib/git/commands/revert.rb +2 -2
  154. data/lib/git/commands/rm.rb +35 -36
  155. data/lib/git/commands/show.rb +1 -1
  156. data/lib/git/commands/show_ref/exclude_existing.rb +3 -4
  157. data/lib/git/commands/show_ref/exists.rb +15 -13
  158. data/lib/git/commands/show_ref/list.rb +39 -35
  159. data/lib/git/commands/show_ref/verify.rb +29 -27
  160. data/lib/git/commands/show_ref.rb +4 -4
  161. data/lib/git/commands/stash/apply.rb +31 -25
  162. data/lib/git/commands/stash/branch.rb +10 -8
  163. data/lib/git/commands/stash/clear.rb +7 -5
  164. data/lib/git/commands/stash/create.rb +10 -8
  165. data/lib/git/commands/stash/drop.rb +15 -9
  166. data/lib/git/commands/stash/list.rb +9 -5
  167. data/lib/git/commands/stash/pop.rb +21 -12
  168. data/lib/git/commands/stash/push.rb +20 -11
  169. data/lib/git/commands/stash/show.rb +21 -12
  170. data/lib/git/commands/stash/store.rb +19 -10
  171. data/lib/git/commands/stash.rb +2 -2
  172. data/lib/git/commands/status.rb +7 -2
  173. data/lib/git/commands/symbolic_ref/delete.rb +18 -14
  174. data/lib/git/commands/symbolic_ref/read.rb +24 -20
  175. data/lib/git/commands/symbolic_ref/update.rb +20 -17
  176. data/lib/git/commands/symbolic_ref.rb +4 -4
  177. data/lib/git/commands/tag/create.rb +45 -42
  178. data/lib/git/commands/tag/delete.rb +5 -3
  179. data/lib/git/commands/tag/list.rb +47 -44
  180. data/lib/git/commands/tag/verify.rb +12 -12
  181. data/lib/git/commands/tag.rb +2 -2
  182. data/lib/git/commands/update_ref/batch.rb +6 -1
  183. data/lib/git/commands/update_ref/delete.rb +19 -21
  184. data/lib/git/commands/update_ref/update.rb +22 -25
  185. data/lib/git/commands/update_ref.rb +4 -4
  186. data/lib/git/commands/version.rb +14 -11
  187. data/lib/git/commands/worktree/add.rb +50 -51
  188. data/lib/git/commands/worktree/list.rb +17 -17
  189. data/lib/git/commands/worktree/lock.rb +12 -12
  190. data/lib/git/commands/worktree/move.rb +16 -16
  191. data/lib/git/commands/worktree/prune.rb +2 -2
  192. data/lib/git/commands/worktree/remove.rb +2 -2
  193. data/lib/git/commands/worktree/repair.rb +2 -2
  194. data/lib/git/commands/worktree/unlock.rb +2 -2
  195. data/lib/git/commands/write_tree.rb +2 -2
  196. data/lib/git/commands.rb +4 -3
  197. data/lib/git/config.rb +47 -1
  198. data/lib/git/configuring.rb +89 -15
  199. data/lib/git/detached_head_info.rb +4 -1
  200. data/lib/git/diff.rb +2 -5
  201. data/lib/git/diff_file_numstat_info.rb +3 -1
  202. data/lib/git/diff_file_patch_info.rb +3 -1
  203. data/lib/git/diff_file_raw_info.rb +3 -1
  204. data/lib/git/diff_info.rb +8 -15
  205. data/lib/git/diff_path_status.rb +16 -3
  206. data/lib/git/diff_result.rb +3 -1
  207. data/lib/git/diff_stats.rb +15 -0
  208. data/lib/git/dirstat_info.rb +24 -8
  209. data/lib/git/encoding_utils.rb +32 -1
  210. data/lib/git/errors.rb +19 -9
  211. data/lib/git/escaped_path.rb +55 -3
  212. data/lib/git/execution_context/repository.rb +15 -0
  213. data/lib/git/execution_context.rb +76 -74
  214. data/lib/git/file_ref.rb +6 -3
  215. data/lib/git/fsck_object.rb +8 -0
  216. data/lib/git/fsck_result.rb +26 -15
  217. data/lib/git/log.rb +152 -5
  218. data/lib/git/object.rb +284 -8
  219. data/lib/git/parsers/branch.rb +18 -0
  220. data/lib/git/parsers/cat_file.rb +2 -2
  221. data/lib/git/parsers/diff.rb +213 -5
  222. data/lib/git/parsers/fsck.rb +11 -0
  223. data/lib/git/parsers/ls_remote.rb +1 -1
  224. data/lib/git/parsers/stash.rb +51 -0
  225. data/lib/git/parsers/tag.rb +38 -0
  226. data/lib/git/remote.rb +34 -2
  227. data/lib/git/repository/branching.rb +23 -21
  228. data/lib/git/repository/committing.rb +46 -6
  229. data/lib/git/repository/context_helpers.rb +45 -16
  230. data/lib/git/repository/diffing.rb +32 -23
  231. data/lib/git/repository/factories.rb +153 -28
  232. data/lib/git/repository/inspecting.rb +1 -1
  233. data/lib/git/repository/logging.rb +169 -30
  234. data/lib/git/repository/maintenance.rb +1 -1
  235. data/lib/git/repository/merging.rb +3 -3
  236. data/lib/git/repository/object_operations.rb +108 -14
  237. data/lib/git/repository/remote_operations.rb +150 -171
  238. data/lib/git/repository/shared_private.rb +7 -5
  239. data/lib/git/repository/staging.rb +36 -12
  240. data/lib/git/repository/stashing.rb +16 -16
  241. data/lib/git/repository/status_operations.rb +1 -1
  242. data/lib/git/repository/worktree_operations.rb +1 -1
  243. data/lib/git/repository.rb +287 -6
  244. data/lib/git/stash.rb +16 -21
  245. data/lib/git/stash_info.rb +4 -4
  246. data/lib/git/stashes.rb +20 -26
  247. data/lib/git/status.rb +25 -13
  248. data/lib/git/tag_delete_failure.rb +3 -0
  249. data/lib/git/tag_delete_result.rb +6 -3
  250. data/lib/git/tag_info.rb +1 -0
  251. data/lib/git/url.rb +12 -5
  252. data/lib/git/version.rb +6 -4
  253. data/lib/git/version_constraint.rb +4 -0
  254. data/lib/git.rb +400 -314
  255. data/redesign/3_architecture_implementation.md +81 -30
  256. data/redesign/Phase 4 - Step A.md +6 -0
  257. data/redesign/Phase 4 - Step B.md +921 -0
  258. data/redesign/Phase 4 - Step C.md +833 -0
  259. data/redesign/c1a-public-api-scope.tsv +256 -0
  260. data/redesign/phase-4-step-b-test-audit.tsv +485 -0
  261. data/tasks/yard.rake +24 -17
  262. metadata +24 -58
  263. data/commitlint.test +0 -4
  264. data/lib/git/args_builder.rb +0 -103
  265. data/lib/git/command_line_result.rb +0 -8
  266. data/lib/git/deprecation.rb +0 -9
  267. data/lib/git/repository/configuring.rb +0 -351
  268. data/tasks/test.rake +0 -25
  269. /data/.github/{skills → skills-deprecated}/extract-facade-from-base-lib/KEYWORD_ARG_REMEDIATION.md +0 -0
  270. /data/.github/{skills → skills-deprecated}/review-backward-compatibility/SKILL.md +0 -0
@@ -0,0 +1,921 @@
1
+ # Phase 4 / Step B — Finalize Test Suite: Execution Plan
2
+
3
+ > **✅ Status: Complete.** W1 (the legacy-test audit) is **complete and verified**;
4
+ > its results are captured below and in the companion data file
5
+ > [`phase-4-step-b-test-audit.tsv`](phase-4-step-b-test-audit.tsv). **W1.5 (matrix
6
+ > enrichment) is complete and signed off** — the TSV now carries `disposition`,
7
+ > `target_spec`, and `port_status`; results are summarized in
8
+ > [W1.5 enrichment results](#w15-enrichment-results-for-sign-off). The one-time human
9
+ > sign-off that gates W2 has been **given**. Execution decisions are
10
+ > **resolved** (see [Resolved Decisions](#resolved-decisions)). **W-Doc (documentation
11
+ > salvage) is complete and merged** — the 4 `test_archive.rb` content cases were salvaged
12
+ > as YARD examples on `Git::Object#archive` / `Git::Branch#archive`. **W2 (port) is
13
+ > complete:** all **159 PORT rows** (down from the planned 192 after dedup) have been
14
+ > ported and merged across batches U1–U21 (final batch PR #1501) — see
15
+ > [W2 progress](#w2-progress). **W3** (remove Test::Unit infrastructure) **is merged** —
16
+ > PR #1502 removed `tests/units/`, tooling, and the `test-unit` gem dependency; PR #1503
17
+ > followed up with the `tests/files/` fixture deletion deferred from W3 due to the
18
+ > 300-file review limit. **W4a** (deprecate `extract-*` skills) **is merged** — PR #1504.
19
+ > **W4b** (remove Test::Unit documentation references) **is merged** — PR #1505. The
20
+ > **W6 plan** (remove stale `Git::Base`/`Git::Lib` skill references) **is merged** —
21
+ > PR #1506. **W5** (verify & finalize) is complete — gate is green: 5668 unit + 893 integration examples, 0 failures. **W6 execution** (remove stale `Git::Base`/`Git::Lib` skill references) is merged — [PR #1509](https://github.com/ruby-git/ruby-git/pull/1509).
22
+
23
+ ## Goal
24
+
25
+ Make RSpec the **sole** test framework for the gem. Port every still-valuable
26
+ Test::Unit test in `tests/units/` to RSpec, then remove the legacy Test::Unit
27
+ suite, its tooling, its gem dependency, and all documentation references to it.
28
+
29
+ Source of truth in the implementation tracker:
30
+ [`3_architecture_implementation.md`](3_architecture_implementation.md) →
31
+ "Phase 4 → Step B — Finalize test suite".
32
+
33
+ This is the second half of Phase 4. [Step A](Phase%204%20-%20Step%20A.md) removed
34
+ the old `Git::Base` / `Git::Lib` code; Step B removes the old test framework that
35
+ those classes were originally written against.
36
+
37
+ ---
38
+
39
+ ## Done-When Criteria
40
+
41
+ - The `tests/` directory is removed.
42
+ - `test-unit` is no longer a dependency in `git.gemspec`.
43
+ - RSpec is the sole test framework: no `test` / `test-all` Rake tasks, and
44
+ `rake default` is green without a Test::Unit step.
45
+ - No surviving runtime/tooling references to `bin/test*`, `tests/units`, or
46
+ `Test::Unit`. Historical/reference mentions are allowed only in `CHANGELOG.md`,
47
+ redesign historical docs, and `.github/skills-deprecated/`.
48
+ - All **unique** behavioral coverage from `tests/units/` now lives in `spec/`,
49
+ placed in the correct layer (logic in `spec/unit/`, real-git wiring in
50
+ `spec/integration/`), except rows explicitly classified as `intentional_drop`,
51
+ `document`, or `obsolete` in the enriched audit matrix (`document` rows are preserved
52
+ as documentation rather than specs).
53
+ - Full quality gate green: `bundle exec rake default:parallel`
54
+ (RSpec unit + integration + RuboCop + YARD + build).
55
+
56
+ ---
57
+
58
+ ## Testing Philosophy (governs the port)
59
+
60
+ Per the `rspec-unit-testing-standards` skill and the project's layering:
61
+
62
+ - **`spec/unit/`** — pure logic with **no external dependencies** (no real git,
63
+ no subprocesses). Externals are stubbed. Must achieve **100% branch coverage**.
64
+ This is where option/argument building, output parsing, value-object behavior,
65
+ error mapping, and deprecation-warning emission belong.
66
+ - **`spec/integration/`** — **minimal** checks that the layers are wired together
67
+ correctly against real git. These exist only to prove the facade → command →
68
+ subprocess path is connected; they do not re-test logic already covered by unit
69
+ specs, and they do not recreate the legacy exhaustive matrices.
70
+
71
+ The W1 audit assigns every PORT case a `target_layer` according to this philosophy,
72
+ so the bulk of legacy "integration-style" assertions collapse into unit specs, with
73
+ only a thin residue remaining as true integration checks.
74
+
75
+ ---
76
+
77
+ ## W1 — Audit & Categorize (✅ complete, verified)
78
+
79
+ Every legacy test was classified at **per-test-method granularity**, not per file: a
80
+ file-level verdict would hide the common case where a file mixes already-covered
81
+ methods with one or two methods carrying unique coverage — and a file is only safe to
82
+ delete if *every* method in it is covered.
83
+
84
+ All **484 individual test cases** (307 `def test_*` methods + 177 declarative
85
+ `test '...' do` macros, across 70 files) received their own verdict. The full matrix
86
+ is persisted as [`phase-4-step-b-test-audit.tsv`](phase-4-step-b-test-audit.tsv).
87
+ The **W1 baseline** carried 10 columns (`file, line, style, test_name,
88
+ initial_verdict, final_verdict, target_layer, spec_coverage, verify_notes, behavior`);
89
+ **W1.5 enrichment appended three more** (`disposition, target_spec, port_status`), so the
90
+ file on disk is now the enriched **13-column** version — see
91
+ [W1.5 enrichment results](#w15-enrichment-results-for-sign-off).
92
+
93
+ The audit ran in two passes:
94
+
95
+ 1. **First pass** — each case classified REDUNDANT (covered by existing specs, or
96
+ intentionally dropped/obsolete) or PORT (unique coverage). When a single existing spec
97
+ cleanly covers a REDUNDANT case, that spec is cited in `spec_coverage`; when coverage
98
+ is **diffuse across multiple specs**, or the case is an **intentional drop / obsolete**,
99
+ `spec_coverage` is `none` and the citation or rationale lives in `verify_notes`. W1.5's
100
+ `disposition` column formalizes the covered / intentional_drop / obsolete distinction.
101
+ 2. **Layer-aware verification pass** — re-checked every verdict against the testing
102
+ philosophy above, assigning each PORT a `target_layer` (`unit` vs `integration`)
103
+ and correcting verdicts that the first pass got wrong.
104
+
105
+ ### Verified totals
106
+
107
+ These are the **W1** outputs (first pass + verification). W1.5 enrichment later reconciled
108
+ them to **292 REDUNDANT / 192 PORT** (4 archive cases flipped PORT → `document`) — see
109
+ [W1.5 enrichment results](#w15-enrichment-results-for-sign-off).
110
+
111
+ | Verdict | Count | Meaning |
112
+ | --- | ---: | --- |
113
+ | **REDUNDANT** | **288** | Already covered, intentionally dropped, or obsolete → classify with `disposition` before deleting |
114
+ | **PORT → unit** | **158** | Unique logic → add a mocked `spec/unit/` example |
115
+ | **PORT → integration** | **38** | Genuinely needs real git/process → add a *minimal* `spec/integration/` example |
116
+ | **PORT (total)** | **196** | |
117
+
118
+ ### Verification corrected 56 verdicts vs. the first pass
119
+
120
+ - **49 PORT → REDUNDANT** — the first pass over-counted; the logic is already
121
+ unit-covered by command/parser/value-object specs, so no heavy end-to-end port is
122
+ warranted.
123
+ - **7 REDUNDANT → PORT** — real coverage gaps the first pass would have **silently
124
+ deleted**:
125
+ - `test_fsck_result.rb:6,36,49` — `Git::FsckResult#initialize` defaults, `#empty?`,
126
+ and `#any_issues?` have no direct unit spec.
127
+ - `test_log.rb:153`, `test_log_execute.rb:134` — `Git::Log#merges` lacks unit
128
+ coverage.
129
+ - `test_status.rb:155,172` — `Git::Status#untracked` collection logic is only
130
+ touched by integration, with no unit coverage.
131
+
132
+ All seven target the **unit** layer and should be prioritized in W2 because they
133
+ close genuine holes.
134
+
135
+ ### Files deletable outright
136
+
137
+ **20 files are 100% REDUNDANT** — every case in them is already covered, so they can
138
+ be deleted with no porting:
139
+
140
+ ```
141
+ test_checkout_index.rb test_ignored_files_with_escaped_path.rb
142
+ test_command_line_result.rb test_index_ops.rb
143
+ test_command_raises_on_failure.rb test_ls_files_with_escaped_path.rb
144
+ test_describe.rb test_ls_tree.rb
145
+ test_each_conflict.rb test_merge.rb
146
+ test_git_binary_version.rb test_merge_base.rb
147
+ test_git_clone.rb test_pull.rb
148
+ test_git_default_branch.rb test_repack.rb
149
+ test_git_path.rb test_show.rb
150
+ test_tags.rb
151
+ test_tree_ops.rb
152
+ ```
153
+
154
+ ### Per-file verdict roll-up
155
+
156
+ Counts are the **post-W1.5 enriched** `REDUNDANT / PORT-unit / PORT-integration / total`.
157
+ Use the TSV for the exact line numbers, behaviors, and verification notes behind each
158
+ count.
159
+
160
+ | File | Red | Unit | Integ | Total |
161
+ | --- | ---: | ---: | ---: | ---: |
162
+ | test_archive.rb | 7 | 1 | 0 | 8 |
163
+ | test_bare.rb | 0 | 0 | 1 | 1 |
164
+ | test_branch.rb | 18 | 0 | 2 | 20 |
165
+ | test_checkout.rb | 8 | 1 | 1 | 10 |
166
+ | test_checkout_index.rb | 8 | 0 | 0 | 8 |
167
+ | test_command_line.rb | 8 | 4 | 5 | 17 |
168
+ | test_command_line_env_overrides.rb | 0 | 14 | 0 | 14 |
169
+ | test_command_line_error.rb | 0 | 2 | 0 | 2 |
170
+ | test_command_line_result.rb | 1 | 0 | 0 | 1 |
171
+ | test_command_raises_on_failure.rb | 4 | 0 | 0 | 4 |
172
+ | test_commit_with_empty_message.rb | 0 | 0 | 2 | 2 |
173
+ | test_commit_with_gpg.rb | 3 | 1 | 0 | 4 |
174
+ | test_config.rb | 4 | 0 | 1 | 5 |
175
+ | test_deprecations.rb | 9 | 10 | 0 | 19 |
176
+ | test_describe.rb | 2 | 0 | 0 | 2 |
177
+ | test_diff.rb | 6 | 11 | 1 | 18 |
178
+ | test_diff_non_default_encoding.rb | 0 | 0 | 2 | 2 |
179
+ | test_diff_path_status.rb | 7 | 1 | 0 | 8 |
180
+ | test_diff_stats.rb | 6 | 1 | 0 | 7 |
181
+ | test_diff_with_escaped_path.rb | 0 | 1 | 0 | 1 |
182
+ | test_each_conflict.rb | 3 | 0 | 0 | 3 |
183
+ | test_escaped_path.rb | 2 | 3 | 0 | 5 |
184
+ | test_failed_error.rb | 0 | 2 | 0 | 2 |
185
+ | test_fsck.rb | 27 | 3 | 0 | 30 |
186
+ | test_fsck_object.rb | 1 | 3 | 0 | 4 |
187
+ | test_fsck_result.rb | 0 | 10 | 0 | 10 |
188
+ | test_git_alt_uri.rb | 0 | 3 | 0 | 3 |
189
+ | test_git_binary_version.rb | 3 | 0 | 0 | 3 |
190
+ | test_git_clone.rb | 7 | 0 | 0 | 7 |
191
+ | test_git_default_branch.rb | 2 | 0 | 0 | 2 |
192
+ | test_git_dir.rb | 1 | 1 | 1 | 3 |
193
+ | test_git_path.rb | 5 | 0 | 0 | 5 |
194
+ | test_ignored_files_with_escaped_path.rb | 1 | 0 | 0 | 1 |
195
+ | test_index_ops.rb | 7 | 0 | 0 | 7 |
196
+ | test_init.rb | 16 | 1 | 0 | 17 |
197
+ | test_log.rb | 5 | 14 | 0 | 19 |
198
+ | test_log_execute.rb | 5 | 16 | 0 | 21 |
199
+ | test_logger.rb | 0 | 2 | 0 | 2 |
200
+ | test_ls_files_with_escaped_path.rb | 1 | 0 | 0 | 1 |
201
+ | test_ls_tree.rb | 1 | 0 | 0 | 1 |
202
+ | test_merge.rb | 5 | 0 | 0 | 5 |
203
+ | test_merge_base.rb | 6 | 0 | 0 | 6 |
204
+ | test_object.rb | 10 | 1 | 0 | 11 |
205
+ | test_object_new.rb | 1 | 2 | 0 | 3 |
206
+ | test_per_instance_config.rb | 2 | 1 | 0 | 3 |
207
+ | test_pull.rb | 6 | 0 | 0 | 6 |
208
+ | test_push.rb | 18 | 0 | 2 | 20 |
209
+ | test_remotes.rb | 21 | 1 | 0 | 22 |
210
+ | test_repack.rb | 1 | 0 | 0 | 1 |
211
+ | test_rm.rb | 8 | 0 | 1 | 9 |
212
+ | test_set_index.rb | 3 | 1 | 1 | 5 |
213
+ | test_set_working.rb | 3 | 1 | 0 | 4 |
214
+ | test_show.rb | 1 | 0 | 0 | 1 |
215
+ | test_signaled_error.rb | 0 | 2 | 0 | 2 |
216
+ | test_signed_commits.rb | 0 | 0 | 1 | 1 |
217
+ | test_stash_list.rb | 6 | 0 | 1 | 7 |
218
+ | test_stash_save.rb | 2 | 0 | 1 | 3 |
219
+ | test_stashes.rb | 4 | 0 | 1 | 5 |
220
+ | test_status.rb | 2 | 10 | 2 | 14 |
221
+ | test_status_object.rb | 0 | 12 | 0 | 12 |
222
+ | test_status_object_empty_repo.rb | 0 | 12 | 0 | 12 |
223
+ | test_submodule.rb | 0 | 0 | 2 | 2 |
224
+ | test_tags.rb | 2 | 0 | 0 | 2 |
225
+ | test_thread_safety.rb | 0 | 0 | 1 | 1 |
226
+ | test_timeout_error.rb | 0 | 2 | 0 | 2 |
227
+ | test_tree_ops.rb | 8 | 0 | 0 | 8 |
228
+ | test_url_clone_to.rb | 0 | 4 | 0 | 4 |
229
+ | test_url_parse.rb | 0 | 3 | 0 | 3 |
230
+ | test_windows_cmd_escaping.rb | 0 | 1 | 0 | 1 |
231
+ | test_worktree.rb | 5 | 0 | 5 | 10 |
232
+ | **Total** | **292** | **158** | **34** | **484** |
233
+
234
+ ---
235
+
236
+ ## Current State (audit findings)
237
+
238
+ - **70** Test::Unit files remain in `tests/units/` (~8,300 lines total).
239
+ - `spec/` already has ~361 spec files; earlier Step A PRs (3a/3b) already deleted the
240
+ `Git::Base` / `Git::Lib`-only Test::Unit files and patched incidental references.
241
+ - **RSpec does NOT depend on `tests/files/` fixtures** — no spec references those
242
+ paths (only stale *comments* point at `tests/units/...`). `tests/files/` can be
243
+ deleted once porting is done.
244
+ - **5 spec files explicitly defer integration coverage to Test::Unit** (high-priority
245
+ porting targets; their comments say "full integration is covered by
246
+ tests/units/..."):
247
+ - `spec/unit/git/object_spec.rb` → `tests/units/test_object.rb`
248
+ - `spec/unit/git/stash_spec.rb`, `spec/unit/git/stashes_spec.rb` → `test_stashes.rb`
249
+ - `spec/unit/git/repository/diffing_spec.rb` +
250
+ `spec/integration/.../diffing_spec.rb` → `test_diff_path_status.rb`,
251
+ `test_diff_stats.rb`
252
+ - `bin/command_line_test` is a fixture script used **only** by
253
+ `tests/units/test_command_line.rb`; its fate is decided when that test is ported.
254
+
255
+ ### Infrastructure / tooling inventory to remove or update
256
+
257
+ | Item | Action |
258
+ | --- | --- |
259
+ | `tests/units/` (70 files) | Port unique coverage, pass pre-delete verification, then delete |
260
+ | `tests/files/` (fixtures) | Delete (RSpec doesn't use them — verify per-fixture) |
261
+ | `tests/Dockerfile`, `tests/docker-compose.yml` | Move to `docker/test/Dockerfile` and `docker/test/docker-compose.yml`; repoint at RSpec |
262
+ | `bin/test`, `bin/test-verbose` | Delete |
263
+ | `bin/test-in-docker` | Rename to `bin/spec-in-docker`; repoint at RSpec entrypoint |
264
+ | `bin/command_line_test` | Relocate into the RSpec support tree (kept as a real shell-out fixture) |
265
+ | `tasks/test.rake` (`test`, `test:parallel`) | Delete |
266
+ | `Rakefile` (`test-all`, `test-all:parallel`, default task lists) | Update to drop `test` |
267
+ | `git.gemspec` (`test-unit ~> 3.7`) | Remove |
268
+ | `.github/copilot-instructions.md` (commands table, suite description) | Update |
269
+ | `CONTRIBUTING.md` (Test::Unit sections, `bin/test` usage) | Update |
270
+ | Skills referencing Test::Unit / `bin/test` / `tests/units` | Update |
271
+ | `.github/workflows/*` | Verify `rake default` still green; no direct `bin/test` calls (currently none) |
272
+
273
+ Skills with Test::Unit references to reconcile: `extract-command-from-lib`,
274
+ `extract-facade-from-base-lib`, `project-context`, `test-debugging`,
275
+ `development-workflow`, `ci-cd-troubleshooting`, `command-test-conventions`.
276
+
277
+ ---
278
+
279
+ ## Workstreams & Sequencing
280
+
281
+ ```mermaid
282
+ flowchart LR
283
+ W1["W1 (audit ✅)"] --> W15["W1.5 (enrich matrix ✅)"]
284
+ W15 --> W2["W2 (port ✅)"]
285
+ W15 --> WDoc["W-Doc (documentation salvage ✅)"]
286
+ W2 --> W3["W3 (remove infra ✅)"]
287
+ WDoc --> W3
288
+ W3 --> W4b["W4b (TU docs ✅)"]
289
+ W4b --> W5["W5 (verify/finalize ✅)"]
290
+ W4a["W4a (deprecate extract-* skills ✅)"] --> W5
291
+ W6["W6 (plan ✅ / exec ✅)"]
292
+ ```
293
+
294
+ - **W1.5 → W2** is a hard one-time gate: the human signs off the enriched matrix once
295
+ before any porting starts.
296
+ - **W2 → W3 → W4b → W5** run in order (W4b's doc claims only become true after W3
297
+ deletes `tests/`).
298
+ - **W-Doc** (documentation salvage of `disposition = document` rows) is gated by the same
299
+ W1.5 sign-off and may run in parallel with W2, but **must merge before W3** — W3 deletes
300
+ `tests/units/`, so the source examples must already be relocated to docs.
301
+ - **W4a** (retiring the finished `extract-*` skills) is independent of W2/W3 and may
302
+ land at any time; W5 waits on W4a as well so the final verification sees the retired
303
+ skills.
304
+
305
+ ### W1.5 — Enrich audit matrix for execution (before W2)
306
+
307
+ Before any W2 porting starts, extend the audit matrix so an agent can execute without
308
+ guessing and the human reviewer can validate each case quickly.
309
+
310
+ - Add a **`disposition`** column for non-ported rows. Keep `final_verdict` stable unless
311
+ the W1.5 sign-off explicitly approves a verdict flip (as it did for the 4 archive
312
+ `document` rows), and distinguish why a REDUNDANT row is safe to delete:
313
+ - `covered` — behavior is already covered by an existing cited spec.
314
+ - `intentional_drop` — behavior is deliberately not ported because it is flaky,
315
+ obsolete, an exhaustive real-git matrix, or otherwise outside the RSpec testing
316
+ philosophy, **and** the example carries no residual user value.
317
+ - `document` — the case is **not** kept as a spec, but its *usage example* has
318
+ user-facing value, so it is converted into documentation (a YARD `@example` or a
319
+ README usage section) instead of being discarded. This is an `intentional_drop` whose
320
+ "how" is worth preserving; W-Doc relocates it.
321
+ - `obsolete` — behavior no longer applies after the architectural redesign.
322
+ PORT rows may use `port` or leave `disposition` blank.
323
+
324
+ **Pass-through vs. parse rule (when content checks become `intentional_drop` /
325
+ `document`).** A legacy test that asserts on the *content* git produces is testing
326
+ **git**, not the gem, when the gem is a pure **pass-through** — it forwards options as
327
+ CLI flags and hands back git's bytes without inspecting them (e.g. `archive` writes a
328
+ tar/tgz the gem never parses). Once that gem's option→flag forwarding is unit-covered
329
+ and its byte-capture-to-file is integration-covered, the residual content assertion adds
330
+ no gem coverage → classify it `intentional_drop`, or `document` when the example teaches
331
+ the user something. When the gem instead **parses or transforms** git output into Ruby
332
+ objects (diff, status, log, branch/tag metadata, encoding, fsck), the content assertion
333
+ exercises the gem's parser → keep it as **PORT**. Flag any PORT→REDUNDANT flip this rule
334
+ produces for the one-time enriched-matrix sign-off.
335
+ - Add a **`target_spec`** column naming the **destination** for every actionable row. For
336
+ PORT rows it is a spec path — prefer extending an existing spec file that mirrors the
337
+ source under test; only create a new spec file when no suitable target exists. For
338
+ `document` rows it is the **doc destination** instead, written as `yard:<Class>#<method>`
339
+ (add/extend a YARD `@example`) or `readme:<section>` (a README usage section). The
340
+ `disposition` value disambiguates how to read the column, so no separate doc column is
341
+ needed. This is required before W2 so placement is deterministic and reviewable.
342
+ - **Split rows by destination, not by individual example.** The unit of work is a
343
+ **destination** — one `(target_layer, target_spec)` pair — and a single row may produce
344
+ **multiple `it` examples** in that one spec (porting a legacy test is rarely 1:1 with a
345
+ single example; those examples are reviewed, committed, and tracked together as one row).
346
+ Split a legacy test into multiple rows **only when it maps to more than one
347
+ destination**:
348
+ - **Multiple specs, same layer** (e.g. its examples belong in two different `spec/unit/`
349
+ files) → one row per `target_spec`.
350
+ - **Multiple layers** — a single legacy test can exercise both logic (→ a mocked
351
+ `spec/unit/` example) and real-git wiring (→ a minimal `spec/integration/` example) →
352
+ one row per layer. The layer-aware W1 pass already assigned PORT to the one layer with
353
+ a genuine gap and marked the other layer's behavior as already-covered, so the 196
354
+ starting rows each carry a single `target_layer`; this is the escape hatch if
355
+ enrichment or W2 finds a case that truly needs both.
356
+
357
+ To split, **add one matrix row per `(target_layer, target_spec)`** — duplicate `file`,
358
+ `line`, and `test_name`, give each row its own `target_layer` / `target_spec` /
359
+ `port_status`, and cross-reference the siblings in `verify_notes`. This preserves the
360
+ core invariant that **one row = one destination (`target_layer` + `target_spec`) = one
361
+ review gate = one commit = one `port_status`** (the mirror image of the dedup/`grouped`
362
+ rule, which merges rows). A single row never spans more than one destination.
363
+ - Add a **`port_status`** column — the single, authoritative progress tracker for every
364
+ actionable row (PORT rows in W2, and `document` rows in W-Doc). It lives **only** in
365
+ this TSV; the prose plans (this document and the session plan) describe the protocol but
366
+ never maintain their own per-row status, so there is exactly one source of truth.
367
+ Initialize it during W1.5:
368
+ - PORT and `document` rows → `pending`.
369
+ - `covered` / `intentional_drop` / `obsolete` rows → leave blank (their fate is governed
370
+ by `disposition`; no further action follows).
371
+ See [W2](#w2--port-unique-coverage-to-rspec-batched-prs) and
372
+ [W-Doc](#w-doc--documentation-salvage-disposition--document) for exactly when the agent
373
+ advances each value.
374
+ - Define W2 PR batches from `target_spec` / component groupings. A batch must contain
375
+ **no more than 10 matrix rows** (the cap counts *work items / examples*, i.e. enriched
376
+ matrix rows after any splits and approved merges — not original legacy tests). If a
377
+ target spec has more than 10 rows, split it into multiple PRs by method/context so each
378
+ review remains small.
379
+ - For integration rows, pre-identify likely deduplication groups where multiple legacy
380
+ rows can be satisfied by one minimal wiring example. These groups are proposals only;
381
+ W2 still requires explicit reviewer approval before grouping rows.
382
+
383
+ **Done-when (hard gate before any W2 porting begins):**
384
+
385
+ - Every PORT row has a non-empty `target_spec`, and any case needing more than one layer
386
+ has been split into one row per `(target_layer, target_spec)`.
387
+ - Every non-PORT row has a `disposition` (`covered`, `intentional_drop`, `document`, or
388
+ `obsolete`); every `document` row has a `target_spec` doc destination.
389
+ - Every PORT row and every `document` row has `port_status = pending`; proposed dedup
390
+ groups are recorded.
391
+ - The proposed W2 batches (≤ 10 matrix rows each) are listed, and the final PORT
392
+ work-item count (after splits/merges) is reconciled in the TSV — the 196 figure is the
393
+ W1 starting count and may shift during enrichment.
394
+ - **The human reviewer approves the enrichment once, before W2 starts — reviewing a
395
+ generated review summary, not the raw 484-row TSV.** A line-by-line read of the matrix
396
+ is not an effective review, so the gate artifact is a concise summary
397
+ ([W1.5 enrichment results](#w15-enrichment-results-for-sign-off)) that surfaces only the
398
+ decisions that carry risk: every PORT→REDUNDANT verdict flip, every `intentional_drop`
399
+ and `obsolete` row, every `document` row and its doc target, and every **new** spec file
400
+ the ports will create. The bulk `covered` and ordinary `port` rows are summarized in
401
+ aggregate, not individually. This front-loads the placement/disposition decisions into a
402
+ single review so they are not re-litigated at the per-case gates. W2 MUST NOT begin
403
+ until this approval is given.
404
+
405
+ #### W1.5 enrichment results (for sign-off)
406
+
407
+ The full pass is complete; the enriched TSV has 13 columns (`disposition`, `target_spec`,
408
+ `port_status` appended). Reconciled totals:
409
+
410
+ | Metric | W1 start | After W1.5 |
411
+ | --- | ---: | ---: |
412
+ | PORT | 196 | **192** |
413
+ | REDUNDANT | 288 | **292** |
414
+ | — of which `covered` | — | 287 |
415
+ | — of which `document` | — | 4 |
416
+ | — of which `intentional_drop` | — | 1 |
417
+ | Rows with `port_status = pending` (PORT + document) | — | 196 |
418
+
419
+ **Verdict flips to review (the risk surface):**
420
+
421
+ - **4 PORT → REDUNDANT/`document`** — all `test_archive.rb` content-validation cases, by
422
+ the pass-through rule (the gem forwards `--format`/`--prefix` and hands back git's bytes
423
+ it never inspects; forwarding is unit-covered, byte-capture is integration-covered).
424
+ Salvaged as YARD examples: L41/62/80 → `yard:Git::Object#archive`, L93 →
425
+ `yard:Git::Branch#archive`.
426
+ - **1 `intentional_drop`** — `test_git_clone.rb:15` (flaky real-clone global-timeout test;
427
+ timeout plumbing is unit-covered by the ExecutionContext/CommandLine specs).
428
+ - **0 `obsolete`.**
429
+
430
+ **New spec files the ports will create** (no existing mirror; all error/cross-cutting):
431
+ `spec/unit/git/command_line_error_spec.rb` (2 rows),
432
+ `spec/unit/git/escaped_path_spec.rb` (3), `spec/unit/git/failed_error_spec.rb` (2),
433
+ `spec/unit/git/signaled_error_spec.rb` (2), `spec/unit/git/timeout_error_spec.rb` (2),
434
+ `spec/integration/git/thread_safety_spec.rb` (1). All other 180 PORT rows extend an
435
+ existing spec.
436
+
437
+ **Proposed dedup group (needs reviewer approval in W2):** `test_log.rb` and
438
+ `test_log_execute.rb` carry the same 14 query-builder test names, both targeting
439
+ `spec/unit/git/log_spec.rb`; they are near-duplicate sources that likely collapse to one
440
+ set of examples (`test_log_execute.rb` adds 2 unique `#execute` snapshot/empty-repo cases).
441
+ Recorded as a proposal; rows are not pre-merged.
442
+
443
+ **Initial W2 batch plan** (all row counts are enriched matrix rows before deduplication
444
+ is approved by a reviewer; each batch stays at or below the 10-row cap):
445
+
446
+ | Batch | Scope | Rows | Status |
447
+ | --- | --- | ---: | --- |
448
+ | U1 | CommandLine base/capturing/error rows | 9 | ✅ merged |
449
+ | U2 | Git error objects (`FailedError`, `SignaledError`, `TimeoutError`) | 6 | ✅ merged |
450
+ | U3 | `Diff` unit rows, first chunk | 10 | ✅ merged |
451
+ | U4 | `Diff` remainder plus path/status/stats/escaped-path rows | 7 | ✅ merged |
452
+ | U5 | `ExecutionContext` environment override rows, first chunk | 7 | ✅ merged |
453
+ | U6 | `ExecutionContext` remainder plus config/init/path-resolver rows | 10 | ✅ merged |
454
+ | U7 | Git remote URL utilities | 10 | ✅ merged |
455
+ | U8 | `Log` deprecation rows plus first `test_log.rb` chunk | 10 | ✅ merged |
456
+ | U9 | `Log` remaining `test_log.rb` rows | 10 | ✅ merged |
457
+ | U10 | `Log#execute` snapshot/empty-repo rows (deduped against `test_log.rb`) | 10 | ✅ merged |
458
+ | U11 | `Log#execute` remainder | 6 | ➖ folded into U10 (dedup) |
459
+ | U12 | Object/repository/remote miscellaneous rows | 10 | ✅ merged |
460
+ | U13 | Repository staging/committing rows | 3 | ✅ merged |
461
+ | U14 | `Fsck` parser rows, first chunk | 10 | ✅ merged |
462
+ | U15 | `Fsck` parser remainder | 6 | ✅ merged |
463
+ | U16 | `Status` facade rows plus ignore-case rows | 10 | ✅ merged |
464
+ | U17 | `StatusFile` rows, first chunk | 10 | ✅ merged |
465
+ | U18 | `StatusFile` remainder plus empty-repo first chunk | 10 | ✅ merged |
466
+ | U19 | `StatusFile` empty-repo remainder | 4 | ✅ merged |
467
+ | U20 | Signed-commits, stash, status, submodule integration rows | 9 | ✅ merged |
468
+ | U21 | Thread-safety, worktree integration rows (final batch) | 14 | ✅ merged (PR #1501) |
469
+ | I1 | Command-line subprocess integration rows | 5 | ✅ merged (folded into U batches) |
470
+ | I2 | Branch/checkout integration rows | 3 | ✅ merged (folded into U batches) |
471
+ | I3 | Commit/config/remote integration rows | 7 | ✅ merged (folded into U batches) |
472
+ | I4 | Diff/status/staging integration rows | 7 | ✅ merged (folded into U batches) |
473
+ | I5 | Object/stash/submodule integration rows | 6 | ✅ merged (folded into U batches) |
474
+ | I6 | Worktree/thread-safety integration rows | 6 | ✅ merged (folded into U batches) |
475
+
476
+ ### W2 — Port unique coverage to RSpec (batched PRs)
477
+
478
+ #### W2 progress
479
+
480
+ Snapshot from the authoritative `port_status` column in
481
+ [`phase-4-step-b-test-audit.tsv`](phase-4-step-b-test-audit.tsv):
482
+
483
+ | Metric | Rows |
484
+ | --- | ---: |
485
+ | **Total PORT rows** (current, after dedups) | **159** |
486
+ | — unit (`target_layer = unit`) | 123 |
487
+ | — integration (`target_layer = integration`) | 36 |
488
+ | **Complete** (`ported`) | **159** (123 unit + 36 integration) |
489
+ | **Remaining** (`pending`) | **0** |
490
+ | **Percent complete** | **100%** (159 / 159) |
491
+
492
+ Batches **U1–U21** are all merged. U11 was folded into U10 during the `test_log.rb` /
493
+ `test_log_execute.rb` dedup. The PORT total dropped from the W1.5 figure of **192** to
494
+ **159** because ~33 rows were reclassified PORT → REDUNDANT (`covered`) as in-flight
495
+ dedups confirmed the behavior was already covered by an existing or sibling-batch spec.
496
+ All 159 PORT rows have `port_status = ported` in the TSV. Note: `ported` became the de
497
+ facto terminal state for PORT rows in this project — merged batches were not
498
+ systematically bumped through `reviewed` → `merged` in the TSV, so `ported` represents
499
+ done. W2 is **complete**.
500
+
501
+ Drive W2 entirely from the enriched audit matrix; port only the **PORT** cases (159 after
502
+ dedup; 192 at W1.5 sign-off), honoring each row's `target_layer` and `target_spec`.
503
+
504
+ **Execution protocol — one test at a time, with review gates.** Each W2 **batch** (not
505
+ all of W2) is delivered as **one PR** with no more than 10 matrix rows; W3–W5 follow
506
+ their own workstream-level PR rules below. The per-case work happens on the topic branch
507
+ *before* that batch PR is opened: the agent advances one discrete item at a time and
508
+ **stops for human review** after each, only continuing on an explicit go-ahead. The
509
+ per-step review gates and the single-batch-PR delivery are not in tension — the branch
510
+ accumulates locally-reviewed commits, and the PR is opened **once, at the end of the
511
+ batch**, as a single ready unit (see [Branch & PR lifecycle](#branch--pr-lifecycle)
512
+ below). For W2, the discrete item is **exactly one legacy audit row**. Multiple audit
513
+ rows may be grouped only when the agent first proposes the grouping and the reviewer
514
+ explicitly approves it. Concretely, per case:
515
+
516
+ 1. Announce the case (file:line, behavior, `target_layer`, `target_spec`) about to be
517
+ ported.
518
+ 2. Write/extend the RSpec example(s) for that destination row (a row may produce more
519
+ than one `it`; plus any minimal support).
520
+ - **2a. Self-check against `rspec-unit-testing-standards` before reporting green.**
521
+ Confirm the example conforms to the skill: for unit rows — externals stubbed, **no
522
+ real git / no subprocesses**, deterministic (no time/network/filesystem ordering
523
+ dependencies), one behavior per example, project naming/structure conventions, and
524
+ 100% branch coverage for the touched logic; for integration rows — minimal real-git
525
+ wiring only, no exhaustive matrices. RuboCop does **not** enforce most of these, so
526
+ this is a manual conformance step, not a lint pass.
527
+ 3. Run just that spec (and coverage for the touched file/class) to show it green, then
528
+ set `port_status = ported` for the row in the audit TSV.
529
+ 4. **Pause** and present the handoff below; wait for "continue" before the next case.
530
+ When the reviewer approves the case, set `port_status = reviewed`.
531
+
532
+ **`port_status` state machine (agent updates the audit TSV — the only tracker):**
533
+
534
+ - `pending` → `ported` — after step 3 (example written and green locally).
535
+ - `ported` → `reviewed` — after the reviewer approves the case at its gate (step 4).
536
+ - `reviewed` → `merged` — after the PR containing the case merges.
537
+ - any → `grouped` — when a row is folded into another row's example under an
538
+ approved dedup group; record the surviving row's `file:line` in `verify_notes` so the
539
+ fold-in is traceable. A `grouped` row needs no example of its own.
540
+
541
+ Update `port_status` **only** in the audit TSV. Do not restate per-row status in this
542
+ document or the session plan — they describe the protocol, the TSV holds the state.
543
+
544
+ #### Branch & PR lifecycle
545
+
546
+ The per-case "review" gates above are **local, in-session** reviews on the topic branch;
547
+ they are distinct from the eventual GitHub PR review. The git mechanics for each batch:
548
+
549
+ 1. **Before the first case:** confirm the working branch is a topic branch, not `main`
550
+ or `4.x` (`git branch --show-current`); if not, `git switch -c <type>/<short-desc>`
551
+ first. One topic branch per batch.
552
+ 2. **Per approved case:** after the reviewer says "continue" and `port_status` is set to
553
+ `reviewed`, commit that case as **one Conventional-Commits commit** (e.g.
554
+ `test: port <area> <behavior> to RSpec`). One commit per reviewed case keeps the
555
+ branch history mirroring the review loop and keeps work safe.
556
+ 3. **At end of batch (all cases `reviewed`):** run the PR-exit gate (cumulative 100%
557
+ branch coverage + RuboCop + specs green), then open **one** ready PR for the batch.
558
+ Do **not** open the PR mid-batch.
559
+ 4. **After merge:** set `port_status = merged` for every row in the batch.
560
+
561
+ W3–W5 follow the same branch-then-one-PR pattern (W3 additionally ships its steps as a
562
+ single atomic PR — see W3).
563
+
564
+ Per-case handoff template:
565
+
566
+ ```
567
+ Legacy case: <file:line> <test_name>
568
+ Behavior: <behavior>
569
+ Target layer: <unit|integration>
570
+ Target spec: <path>
571
+ Examples changed: <brief list>
572
+ Standards: conforms to `rspec-unit-testing-standards` — <rules self-checked: stubbed
573
+ externals / no real git / deterministic / one-behavior-per-example / naming &
574
+ structure / coverage>
575
+ Commands run: <commands + results>
576
+ Coverage: <line/branch result for touched class/file>
577
+ Port status: <pending|ported|reviewed|merged|grouped>
578
+ Diff summary: <files changed + intent>
579
+ Status: waiting for review; say "continue" to proceed
580
+ ```
581
+
582
+ This keeps each review small and lets the reviewer catch convention drift early.
583
+
584
+ - **`target_layer = unit`** (123 now; 158 at W1.5 sign-off) → write/extend `spec/unit/`
585
+ specs with stubbed externals (no real git), preserving 100% branch coverage. This is the
586
+ bulk of the work and where logic coverage belongs.
587
+ - **`target_layer = integration`** (36 now; 34 at W1.5 sign-off) → add only **minimal**
588
+ `spec/integration/` examples that prove the facade ↔ command wiring against real git; do
589
+ not recreate the legacy exhaustive matrices.
590
+ - **Integration deduplication rule:** before adding an integration example, check
591
+ whether a minimal wiring example for the same path already exists or is proposed in
592
+ the current batch. Folding multiple audit rows into one integration example is allowed
593
+ only after the agent proposes the grouping and the reviewer explicitly approves it.
594
+ - Follow `facade-test-conventions` / `command-test-conventions` /
595
+ `rspec-unit-testing-standards`. Query the matrix per file/area for exact line
596
+ numbers, behaviors, `verify_notes`, `target_spec`, and `disposition`.
597
+ - Remove the now-satisfied "covered by tests/units/..." deferral comments from the
598
+ 5 spec files as their coverage lands.
599
+ - Special handling:
600
+ - `test_command_line.rb` lines 69, 126, 186, 212, 269 + `test_thread_safety.rb:22`
601
+ are the 6 integration cases that exercise real subprocess timeout/signal/IO via
602
+ `bin/command_line_test`. **Decision (Q4):** keep a real shell-out and **relocate**
603
+ the fixture into the RSpec support tree (see W3) — do *not* collapse it into an
604
+ inline `ruby -e` one-liner.
605
+ - The 7 coverage-gap cases (FsckResult helpers, `Git::Log#merges`,
606
+ `Git::Status#untracked`) are unit ports that close real gaps — prioritize them.
607
+ - encoding / escaped-path tests — port fixtures or construct repos in-spec.
608
+ - Coverage expectations — distinguish the two checkpoints so the agent does not chase
609
+ 100% on a single case or get blocked mid-batch:
610
+ - **Per case (informational):** report the line/branch coverage for each touched
611
+ class/file in the handoff. A single ported case is not expected to reach 100% on
612
+ its own; the number is feedback, not a gate.
613
+ - **PR exit (the gate):** the batch's cumulative coverage must be 100% branch
614
+ coverage before the PR is opened, with RuboCop clean and all touched specs green.
615
+ Because SimpleCov does not yet strictly fail on low coverage, the agent must verify
616
+ and state the final cumulative coverage explicitly at PR exit.
617
+ - **Standards conformance is a per-case acceptance criterion, separate from coverage and
618
+ RuboCop.** Every ported case must pass the step 2a self-check against
619
+ `rspec-unit-testing-standards`, and the handoff must record which rules were checked, so
620
+ the reviewer can confirm conformance at the gate. Green specs + clean RuboCop are
621
+ necessary but **not sufficient** — RuboCop does not enforce the stub-don't-shell-out,
622
+ determinism, one-behavior-per-example, or unit-vs-integration boundary rules.
623
+
624
+ ### W-Doc — Documentation salvage (`disposition = document`)
625
+
626
+ Independent of the spec porting in W2, but gated by the same W1.5 sign-off and **must
627
+ merge before W3** (which deletes `tests/units/`, the source of these examples). Delivered
628
+ as one docs-only PR under the same one-at-a-time review cadence as W2.
629
+
630
+ - Process every row with `disposition = document`. The `target_spec` column names the
631
+ destination: `yard:<Class>#<method>` (add/extend a YARD `@example` on that method) or
632
+ `readme:<section>` (a usage section in `README.md`).
633
+ - Per row: (1) announce file:line and the `target_spec` doc destination; (2) write the
634
+ documentation example, derived from the legacy test's usage (the *how*, not its content
635
+ assertions); (3) confirm it renders/builds (YARD example blocks must be valid Ruby;
636
+ `bundle exec rake yard` clean), then set `port_status = ported`; (4) pause for review,
637
+ setting `port_status = reviewed` on approval. After the PR merges, set
638
+ `port_status = merged`.
639
+ - Examples must be runnable and accurate — prefer the minimal, idiomatic gem call the
640
+ legacy test demonstrated (e.g. `git.object('HEAD').archive('out.tgz', format: 'tgz',
641
+ prefix: 'release/')`), not the git-content assertions that motivated the drop.
642
+ - Gate: `bundle exec rake yard build` clean; every processed row is `reviewed`; no
643
+ `document` row is left `pending` before W3's pre-delete gate runs.
644
+
645
+ ### W3 — Remove Test::Unit infrastructure (single atomic PR, after W2)
646
+
647
+ **Status: MERGED.** Infrastructure removal done across two PRs: PR #1502
648
+ (`test/phase-4-step-b-w3-remove-infra` branch: removed `tests/units/`, `bin/test`,
649
+ `bin/test-verbose`, `tasks/test.rake`, the `test-unit` gem dependency, and introduced
650
+ `bin/spec-in-docker` and `docker/test/Dockerfile`) and PR #1503
651
+ (`chore/remove-tests-files-fixtures` branch: deleted the 545 `tests/files/` fixture
652
+ files deferred from W3 due to the Copilot 300-file review limit).
653
+
654
+ W3 follows the same per-step review cadence as W2 (the agent pauses for review after
655
+ each discrete change below), but all of the deletions/relocations ship together as one
656
+ atomic PR — the repository must never be left in a half-removed state on the branch tip.
657
+
658
+ - Delete `tests/units/` and `tests/files/`.
659
+ - Delete `bin/test`, `bin/test-verbose`.
660
+ - Delete `tasks/test.rake`.
661
+ - Update `Rakefile`: remove `test-all` + `test-all:parallel` tasks; drop `test` /
662
+ `test:parallel` from `default_tasks` / `default_tasks_parallel`.
663
+ - Remove `test-unit` from `git.gemspec`; `bundle install` to update `Gemfile.lock`.
664
+ - **Pre-delete verification gate:** before deleting `tests/units/`, re-check the
665
+ non-ported rows from the enriched audit matrix:
666
+ - automated check that every `disposition = covered` row **whose `spec_coverage` cites a
667
+ single spec path** points at a spec file that still exists (202 rows);
668
+ - automated check that every `disposition = covered` row **whose `spec_coverage` cites
669
+ multiple semicolon-separated spec paths** points only at spec files that still exist
670
+ (37 rows);
671
+ - **full** manual review of every `disposition = covered` row whose `spec_coverage` is
672
+ `none` — these are diffusely covered across multiple specs (the citation lives in
673
+ `verify_notes`, so the automated single-path check cannot reach them; 48 rows);
674
+ - manual review of every `disposition = intentional_drop` row (1 row);
675
+ - confirmation that every `disposition = document` row has been relocated to its
676
+ `target_spec` doc destination (W-Doc merged), so deleting the legacy test loses no
677
+ example (4 rows);
678
+ - explicit confirmation that any `obsolete` row is obsolete because of the redesign.
679
+ Do not delete the legacy suite until this gate passes.
680
+ - **Relocate the `command_line_test` fixture (Q4).** The 6 subprocess integration
681
+ cases genuinely shell out of the Ruby process, so the fixture script is kept as a
682
+ real executable but moved out of `bin/` into the RSpec support tree (e.g.
683
+ `spec/support/fixtures/command_line_test`), and the ported specs invoke it via a
684
+ real child process (`ruby <fixture> --stdout=… --signal=9 --duration=… …`). Delete
685
+ the original `bin/command_line_test` once the specs reference the relocated copy.
686
+ - **Preserve an RSpec-based Docker equivalent (Q3).** Do *not* simply delete the
687
+ container tooling. Move `tests/Dockerfile` and `tests/docker-compose.yml` to
688
+ `docker/test/Dockerfile` and `docker/test/docker-compose.yml`, and repoint them at the
689
+ RSpec suite — the container entrypoint runs `bundle exec rake default` (or
690
+ `bundle exec rake spec:parallel`) instead of the Test::Unit `bin/test`. Rename
691
+ `bin/test-in-docker` to **`bin/spec-in-docker`** so the "run the suite in a clean
692
+ container" workflow survives the migration without preserving the old Test::Unit
693
+ name.
694
+ - Gate: `bundle exec rake default:parallel` green;
695
+ repo-wide reference cleanup passes under the scope defined in W4b.
696
+
697
+ ### W4a — Deprecate the finished `extract-*` skills (independent docs-only PR)
698
+
699
+ **Status: MERGED.** PR #1504, `chore/deprecate-extract-skills` branch.
700
+
701
+ This workstream has **no dependency on W2/W3** — the Base/Lib extraction these two
702
+ skills describe is already complete, so they can be retired at any time and this PR may
703
+ land early.
704
+
705
+ - **Deprecate the now-finished `extract-*` skills (Q2).** They should stop loading into
706
+ agent context while remaining available for historical reference. Create a new
707
+ **`.github/skills-deprecated/`** directory (outside the auto-discovered
708
+ `.github/skills/` tree) and `git mv` both skills into it:
709
+ - `.github/skills/extract-command-from-lib/` → `.github/skills-deprecated/extract-command-from-lib/`
710
+ - `.github/skills/extract-facade-from-base-lib/` → `.github/skills-deprecated/extract-facade-from-base-lib/`
711
+
712
+ Add a short `.github/skills-deprecated/README.md` explaining the directory holds
713
+ retired skills kept only for reference (not auto-loaded). Then fix the now-dangling
714
+ inbound "Related skills" links in the **active** skills that point at them:
715
+ - `command-implementation/REFERENCE.md`
716
+ - `facade-implementation/SKILL.md`
717
+ - `development-workflow/SKILL.md`
718
+ - `project-context/SKILL.md`
719
+
720
+ Either drop those bullet links or re-point them at the deprecated location with a
721
+ "(retired)" note. The two skills' *internal* cross-links to each other move together,
722
+ so they keep working.
723
+ - **Re-grep the whole repo after the move.** The four inbound links above are the
724
+ *known* references; before opening the PR, search the entire repository for the two
725
+ skill names (`extract-command-from-lib`, `extract-facade-from-base-lib`) and reconcile
726
+ every hit — not just the four. Any surviving mention outside
727
+ `.github/skills-deprecated/` (and the allowed historical scopes) must be updated to a
728
+ "(retired)" pointer or removed.
729
+ - Gate: the repo-wide grep for both skill names returns only intended hits (the
730
+ relocated skills, deliberate "(retired)" pointers, and allowed historical docs).
731
+
732
+ ### W4b — Remove Test::Unit documentation references (docs-only PR, after W3)
733
+
734
+ **Status: MERGED.** PR #1505, `chore/remove-test-unit-docs` branch.
735
+
736
+ This workstream **depends on W3**: the docs only become true once `tests/` and the
737
+ Test::Unit tooling are actually gone, so it lands after the W3 removal PR merges.
738
+
739
+ - `.github/copilot-instructions.md` — remove Test::Unit rows from the commands table;
740
+ rewrite the "Test suites" paragraph to state RSpec is the sole suite. Update any
741
+ Docker-run command to the relocated RSpec entrypoint.
742
+ - `CONTRIBUTING.md` — remove Test::Unit / `bin/test` sections; keep RSpec guidance;
743
+ point the Docker workflow at the relocated equivalent.
744
+ - Other skills with Test::Unit references to reconcile (update, not deprecate):
745
+ `test-debugging`, `ci-cd-troubleshooting`, `command-test-conventions`.
746
+ - Gate: no live references remain outside the allowed historical/reference scopes:
747
+ `CHANGELOG.md`, redesign historical docs, and `.github/skills-deprecated/`. Active
748
+ code, active docs, active skills, `bin/`, `tasks/`, `spec/`, and `.github/workflows/`
749
+ must not retain live `tests/units`, `bin/test*`, or `Test::Unit` references, except
750
+ deliberate "(retired)" pointers into `.github/skills-deprecated/`.
751
+
752
+ ### W5 — Verify & finalize (after W3, W4a, and W4b)
753
+
754
+ **Status: ✅ Complete.** W3, W4a, and W4b have all merged; full quality gate passed.
755
+
756
+ - Full local gate: `bundle exec rake default:parallel` (and a serial `rake default`
757
+ smoke for JRuby parity if feasible).
758
+ - Confirm W3, W4a, and W4b have all merged so the verification reflects the final
759
+ state (no `tests/` suite, retired `extract-*` skills, Test::Unit-free docs).
760
+ - Confirm CI (`rake default`, `rake test:gem`) is green — `test:gem` is independent of
761
+ Test::Unit and stays.
762
+ - Update [`3_architecture_implementation.md`](3_architecture_implementation.md)
763
+ progress tracker: mark Step B ✅ and bump the Phase 4 percentage.
764
+
765
+ ### W6 — Remove stale `Git::Base`/`Git::Lib` references from active skills (independent docs-only PR)
766
+
767
+ **Status: ✅ Plan merged** (PR #1506) **and execution merged** (PR #1509, `docs/w6-remove-stale-base-lib-skill-refs` branch).
768
+
769
+ This workstream has **no dependency on W2/W3/W4a/W4b/W5** — it is not gated by,
770
+ and does not gate, the Test::Unit removal work above. It exists because Phase 4
771
+ **Step A** (the atomic `Git::Base`/`Git::Lib` removal, PR #1456) only required
772
+ `lib/` source to be clean of live references; `.github/skills/` was never audited
773
+ and still describes those two classes as though they currently exist and are the
774
+ live facade layer.
775
+
776
+ > **Note:** The following is historical context from execution planning. This work was completed in PR #1509.
777
+
778
+ The execution PR for this workstream should be branched from
779
+ `main` after PRs #1504 and #1505 merge, as both touch files in the list below
780
+ and would conflict with concurrent edits.
781
+
782
+ - **Audit every active skill under `.github/skills/`** for `Git::Base` / `Git::Lib`
783
+ mentions that describe them as *current* — as opposed to mentions that are correctly
784
+ historical/contextual (e.g., explaining what the redesign replaced). The following
785
+ files have at least one stale "current" reference and need review/rewrite
786
+ (verified by grep after PRs #1504 and #1505 merged, which cleaned up
787
+ `facade-implementation/SKILL.md` and `development-workflow/SKILL.md`):
788
+ - `command-implementation/SKILL.md`
789
+ - `command-implementation/REFERENCE.md`
790
+ - `command-test-conventions/SKILL.md`
791
+ - `facade-implementation/REFERENCE.md`
792
+ - `facade-test-conventions/SKILL.md`
793
+ - `yard-documentation/SKILL.md`
794
+ - `breaking-change-analysis/SKILL.md`
795
+ - `review-backward-compatibility/SKILL.md`
796
+ - `review-arguments-dsl/CHECKLIST.md`
797
+ - `refactor-command-to-commandlineresult/SKILL.md`
798
+ - **Rewrite each stale reference** to describe the current architecture: `Git::Lib`
799
+ facade-delegation examples (e.g. the now-deleted `lib/git/lib.rb` and
800
+ `spec/unit/git/lib_command_spec.rb`) become `Git::Repository::*` facade examples
801
+ (`lib/git/repository/<topic>.rb`, `spec/unit/git/repository/<topic>_spec.rb`);
802
+ `Git::Base` becomes `Git::Repository`.
803
+ Where a skill's own code example depends on the deleted classes (not just prose),
804
+ rewrite the example against a real current class, following the same approach used
805
+ for the stale TDD-cycle example fixed in W4b.
806
+ - **Do not touch** `redesign/1_architecture_existing.md` and
807
+ `redesign/2_architecture_redesign.md` — those documents intentionally describe the
808
+ pre-redesign architecture as history, and `.github/skills-deprecated/*` — already
809
+ scoped as retired reference material in W4a.
810
+ - Gate: a grep for `Git::Base` and `Git::Lib` across `.github/skills/` returns
811
+ only mentions that are unambiguously historical/contextual, not descriptions
812
+ of current behavior.
813
+
814
+ ---
815
+
816
+ ## Resolved Decisions
817
+
818
+ These were the open questions for Step B; all are now decided and folded into the
819
+ workstreams above.
820
+
821
+ 1. **PR decomposition** → **Batched PRs for W2**, grouped by `target_spec` / component
822
+ with **no more than 10 legacy audit rows per PR**. Within each W2 PR, the agent ports
823
+ one legacy audit row at a time and stops for human review before moving on (the
824
+ per-test review-loop protocol in W2). W3–W5 follow the workstream-specific PR rules
825
+ above.
826
+ 2. **Fate of the `extract-*` skills** → **Deprecate by relocation.** Move both
827
+ completed migration skills to a new `.github/skills-deprecated/` directory (outside
828
+ the auto-discovered `.github/skills/` tree) so they no longer load into agent
829
+ context but remain on disk for reference, with a README marking the directory as
830
+ retired. Inbound "Related skills" links in active skills are dropped or re-pointed
831
+ with a "(retired)" note. (W4a)
832
+ 3. **Docker test tooling** → **Preserve an RSpec-based equivalent.** Relocate the
833
+ Dockerfile / compose file out of `tests/` and repoint the container entrypoint at
834
+ the RSpec suite (`bundle exec rake default` / `spec:parallel`); keep a
835
+ `*-in-docker` runner. (W3)
836
+ 4. **`bin/command_line_test` fixture** → **Keep the real shell-out; relocate the
837
+ fixture.** The subprocess integration cases must genuinely leave the Ruby process
838
+ (to exercise real timeout/signal/exit-status/IO behavior), so an inline `ruby -e`
839
+ one-liner is the wrong fit. Keep the fixture as a real executable script and move it
840
+ into the RSpec support tree; the ported specs invoke it as a child process. (W2/W3)
841
+
842
+ > *On "inline equivalent":* that phrase meant constructing the child program inline
843
+ > in the spec via `ruby -e '<program text>'` instead of shipping a separate script
844
+ > file. It still spawns a genuine subprocess — but because this fixture handles
845
+ > stdout, stderr, exit status, signals, sleep/duration, and file redirection, an
846
+ > inline one-liner would be unreadable. Relocating the real script is cleaner and
847
+ > preserves the genuine shell-out.
848
+ 5. **Non-ported REDUNDANT classification** → keep `final_verdict` stable unless the W1.5
849
+ sign-off explicitly approves a verdict flip; add a `disposition` column (`covered`,
850
+ `intentional_drop`, `document`, `obsolete`) before W2/W3 so deletion decisions are
851
+ auditable.
852
+ 6. **PORT placement** → add a `target_spec` column for every PORT row before W2 so the
853
+ agent does not infer destination files during conversion.
854
+ 7. **One-at-a-time strictness and integration deduplication** → default to one audit row
855
+ per review gate. Group rows only when the agent proposes the grouping and the
856
+ reviewer explicitly approves it; this especially applies to minimal integration
857
+ examples that may satisfy multiple rows.
858
+ 8. **Pre-delete REDUNDANT verification** → automated citation/path checks for `covered`
859
+ rows that cite a single spec path (202) or multiple semicolon-separated spec paths
860
+ (37), **full** manual review for the diffusely covered rows whose `spec_coverage` is
861
+ `none` (48, citation in `verify_notes`), manual review for all `intentional_drop` rows
862
+ (1), confirmation that every `document` row has been relocated to docs (W-Doc merged,
863
+ 4), and explicit confirmation for `obsolete` rows before W3 deletes `tests/units/`.
864
+ 9. **Repo-wide reference cleanup scope** → allow historical/reference mentions only in
865
+ `CHANGELOG.md`, redesign historical docs, and `.github/skills-deprecated/`; clean
866
+ all live references elsewhere.
867
+ 10. **Docker runner naming/location** → relocate container files to `docker/test/` and
868
+ rename the runner to `bin/spec-in-docker`.
869
+ 11. **W2 progress tracking** → add a `port_status` column to the audit TSV
870
+ (`pending → ported → reviewed → merged`, plus `grouped` for rows folded into an
871
+ approved dedup group). The TSV is the **single source of truth** for per-row status;
872
+ the prose plans describe the protocol and the state machine but never maintain a
873
+ parallel per-row status list, so updates are never duplicated across files.
874
+ 12. **W4 split** → W4 is delivered as two independent PRs: **W4a** retires the finished
875
+ `extract-*` skills (no dependency on W2/W3, may land early) and **W4b** removes
876
+ Test::Unit documentation references (depends on W3, since the docs only become true
877
+ once `tests/` is deleted). A repo-wide grep for the two retired skill names is part
878
+ of the W4a gate.
879
+ 13. **PR open timing** → within each W2 batch, all per-case work is reviewed **locally**
880
+ on the topic branch first; the GitHub PR is opened **once, at the end of the batch**,
881
+ after the cumulative coverage gate passes — not incrementally during the loop. Each
882
+ approved case is its own Conventional-Commits commit. W3–W5 follow the same
883
+ branch-then-one-PR pattern. See [Branch & PR lifecycle](#branch--pr-lifecycle).
884
+ 14. **Splitting one legacy test into multiple specs** → the unit of work is a
885
+ **destination** = one `(target_layer, target_spec)` pair. A single row may produce
886
+ **multiple `it` examples** in that one spec (porting is rarely 1:1 with a single
887
+ example); those examples share one review gate, one commit, and one `port_status`.
888
+ Split a legacy test into multiple rows **only when it maps to more than one
889
+ destination** — multiple target specs on the same layer, or multiple layers — adding
890
+ one matrix row per `(target_layer, target_spec)` (shared file/line/test_name;
891
+ independent `target_spec` / `port_status`; siblings cross-referenced in
892
+ `verify_notes`). A row never spans more than one destination. This keeps the invariant
893
+ **one row = one destination = one gate = one commit = one status**, and is the mirror
894
+ image of the dedup/`grouped` merge rule. Consequently the batch cap counts **matrix
895
+ rows (work items)**, not original legacy tests or individual examples, and the 196
896
+ PORT figure is the W1 starting count that may shift as enrichment splits or merges
897
+ rows.
898
+ 15. **Documentation salvage (`document` disposition)** → a legacy case whose behavior is
899
+ not kept as a spec but whose *usage example* has user value is classified
900
+ `disposition = document` (not `intentional_drop`), and its example is converted to
901
+ documentation. This is driven by the **pass-through vs. parse rule**: content
902
+ assertions against git output the gem never inspects (pure pass-through, e.g.
903
+ `archive`) become `intentional_drop` / `document` once forwarding + byte-capture are
904
+ covered; content assertions against output the gem parses into Ruby objects (diff,
905
+ status, log, metadata, encoding, fsck) stay PORT. The doc destination is recorded by
906
+ **reusing the `target_spec` column** (`yard:<Class>#<method>` or `readme:<section>`),
907
+ disambiguated by the `disposition` value, so no extra column is added. These rows are
908
+ delivered by **W-Doc**, a dedicated documentation-only PR that runs under the same
909
+ one-at-a-time gate, is gated by the W1.5 sign-off, and **must merge before W3** so the
910
+ source examples are relocated before the legacy suite is deleted.
911
+
912
+ ---
913
+
914
+ ## Companion Data
915
+
916
+ - [`phase-4-step-b-test-audit.tsv`](phase-4-step-b-test-audit.tsv) — the authoritative
917
+ per-test-case audit matrix (484 rows). W1.5 enriched it into the executable
918
+ work-breakdown input **and** the single per-row progress tracker for W2/W-Doc/W3.
919
+ The file uses `|`-separated fields despite the historical `.tsv` extension. Current
920
+ columns: `file, line, style, test_name, initial_verdict, final_verdict, target_layer,
921
+ spec_coverage, verify_notes, behavior, disposition, target_spec, port_status`.