jobcompat 0.1.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -0
- data/LICENSE +21 -0
- data/README.md +161 -0
- data/SECURITY.md +5 -0
- data/docs/architecture.md +587 -0
- data/docs/competitive-analysis.md +268 -0
- data/docs/implementation-plan.md +806 -0
- data/docs/release-checklist-v0.1.0.md +49 -0
- data/docs/release-notes-v0.1.0.md +24 -0
- data/docs/spec-v0.1.md +1157 -0
- data/exe/jobcompat +3 -0
- data/lib/jobcompat/analysis.rb +340 -0
- data/lib/jobcompat/cli.rb +107 -0
- data/lib/jobcompat/config.rb +89 -0
- data/lib/jobcompat/engine.rb +247 -0
- data/lib/jobcompat/errors.rb +12 -0
- data/lib/jobcompat/formatter.rb +64 -0
- data/lib/jobcompat/git_repository.rb +78 -0
- data/lib/jobcompat/version.rb +3 -0
- data/lib/jobcompat.rb +8 -0
- metadata +84 -0
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
# jobcompat v0.1 implementation plan
|
|
2
|
+
|
|
3
|
+
Status: executable plan for the next Codex session
|
|
4
|
+
Date: 2026-09-23
|
|
5
|
+
Scope guard: this document plans implementation; no product implementation was created in the specification session.
|
|
6
|
+
|
|
7
|
+
## 1. Delivery strategy
|
|
8
|
+
|
|
9
|
+
Implement in small vertical increments. Every phase ends with an observable command or focused test result. Do not begin multi-framework abstractions, Redis integration, ActiveJob, or schema/type analysis.
|
|
10
|
+
|
|
11
|
+
Required execution environment:
|
|
12
|
+
|
|
13
|
+
- Ruby 3.3 or newer (the current machine's system Ruby 2.6 is insufficient and must not be used for implementation validation);
|
|
14
|
+
- Git available on PATH;
|
|
15
|
+
- no Redis, Rails app, Sidekiq server, or network needed for tests.
|
|
16
|
+
|
|
17
|
+
Dependency policy:
|
|
18
|
+
|
|
19
|
+
- runtime: Prism only (`>= 1.9`, `< 2`);
|
|
20
|
+
- CLI: `OptionParser`;
|
|
21
|
+
- config/output/process: standard `Psych`, `JSON`, `Open3`;
|
|
22
|
+
- tests: Minitest and Rake;
|
|
23
|
+
- no new dependency without revisiting `docs/architecture.md` and documenting the need.
|
|
24
|
+
|
|
25
|
+
## 2. Definition of done
|
|
26
|
+
|
|
27
|
+
The implementation is done only when:
|
|
28
|
+
|
|
29
|
+
1. every acceptance criterion in `docs/spec-v0.1.md` passes;
|
|
30
|
+
2. unit, analysis, integration, CLI, and determinism tests pass on supported Ruby versions;
|
|
31
|
+
3. a temporary real Git repository demonstrates exit 0, 1, and 2;
|
|
32
|
+
4. text output is inspected as a user would see it;
|
|
33
|
+
5. JSON output validates against the documented schema fields and stable fixtures;
|
|
34
|
+
6. `git status` of the analyzed fixture proves `jobcompat check` does not mutate it;
|
|
35
|
+
7. README's first screen communicates the five-second example;
|
|
36
|
+
8. no product code supports out-of-scope frameworks or APIs;
|
|
37
|
+
9. packaging builds the gem and the installed executable runs `--help`, `--version`, and `check`;
|
|
38
|
+
10. pre-existing or environment-specific test failures are clearly separated from regressions.
|
|
39
|
+
|
|
40
|
+
## 3. Phase plan
|
|
41
|
+
|
|
42
|
+
### Phase 0 — toolchain and executable skeleton
|
|
43
|
+
|
|
44
|
+
**Files**
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
jobcompat.gemspec
|
|
48
|
+
Gemfile
|
|
49
|
+
Rakefile
|
|
50
|
+
LICENSE
|
|
51
|
+
exe/jobcompat
|
|
52
|
+
lib/jobcompat.rb
|
|
53
|
+
lib/jobcompat/version.rb
|
|
54
|
+
lib/jobcompat/cli.rb
|
|
55
|
+
lib/jobcompat/errors.rb
|
|
56
|
+
test/test_helper.rb
|
|
57
|
+
test/integration/check_command_test.rb
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Implementation tasks**
|
|
61
|
+
|
|
62
|
+
- create a conventional gem without generated framework code;
|
|
63
|
+
- set `required_ruby_version >= 3.3`;
|
|
64
|
+
- add Prism runtime dependency only;
|
|
65
|
+
- add Minitest/Rake development dependencies;
|
|
66
|
+
- define `Jobcompat::VERSION = "0.1.0"`;
|
|
67
|
+
- implement top-level help, version, `check --help`, and structural option validation without pretending analysis completed;
|
|
68
|
+
- define typed expected error classes;
|
|
69
|
+
- make the executable return integer exit codes through `CLI.start`.
|
|
70
|
+
|
|
71
|
+
**Tests**
|
|
72
|
+
|
|
73
|
+
- `jobcompat --help` exits 0 and lists `check`;
|
|
74
|
+
- `jobcompat --version` prints exactly `jobcompat 0.1.0` and exits 0;
|
|
75
|
+
- `jobcompat check --help` documents base/head/format/config;
|
|
76
|
+
- missing `--base` exits 2 with one concise usage error;
|
|
77
|
+
- unknown command/option exits 2;
|
|
78
|
+
- no ANSI escape sequences appear.
|
|
79
|
+
|
|
80
|
+
**Completion criteria**
|
|
81
|
+
|
|
82
|
+
- gem builds locally;
|
|
83
|
+
- executable works through `bundle exec` and built-gem installation in a temp directory;
|
|
84
|
+
- no analysis behavior is faked as complete.
|
|
85
|
+
|
|
86
|
+
### Phase 1 — strict configuration
|
|
87
|
+
|
|
88
|
+
**Files**
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
lib/jobcompat/config.rb
|
|
92
|
+
test/unit/config_test.rb
|
|
93
|
+
test/fixtures/config/*.yml # only if fixture files improve readability
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Implementation tasks**
|
|
97
|
+
|
|
98
|
+
- encode exact default include/exclude arrays;
|
|
99
|
+
- load `.jobcompat.yml` via `Psych.safe_load` with aliases/classes/symbols disabled;
|
|
100
|
+
- validate schema version, types, keys, rule IDs, canonical worker names, reasons, and duplicate suppressions;
|
|
101
|
+
- implement path matching with `FNM_PATHNAME | FNM_EXTGLOB | FNM_DOTMATCH`, explicitly excluding platform case-fold flags;
|
|
102
|
+
- implement exact `rule_id + worker` suppression;
|
|
103
|
+
- retain a matched suppression audit record with configured reason and suppressed finding count;
|
|
104
|
+
- distinguish absent default config from missing explicit config.
|
|
105
|
+
|
|
106
|
+
**Tests**
|
|
107
|
+
|
|
108
|
+
- no config produces documented defaults;
|
|
109
|
+
- valid minimal and full config;
|
|
110
|
+
- include match, exclude match, exclude precedence, root `.rb` file, dot path, nested path;
|
|
111
|
+
- user-provided include/exclude replaces the corresponding defaults;
|
|
112
|
+
- empty exclude clears default exclusions; empty include is rejected;
|
|
113
|
+
- absolute, NUL-containing, and parent-traversal globs are rejected;
|
|
114
|
+
- missing explicit config exits/raises config error;
|
|
115
|
+
- unknown top-level/nested/ignore key rejected;
|
|
116
|
+
- string `"1"` version rejected; version 2 rejected;
|
|
117
|
+
- empty include/exclude item rejected;
|
|
118
|
+
- invalid rule and leading-`::` worker rejected;
|
|
119
|
+
- blank reason rejected;
|
|
120
|
+
- duplicate `(rule, worker)` rejected;
|
|
121
|
+
- YAML alias/object tag rejected;
|
|
122
|
+
- known-worker suppression matches exactly and does not wildcard namespaces.
|
|
123
|
+
|
|
124
|
+
**Completion criteria**
|
|
125
|
+
|
|
126
|
+
- Config has no dependency on CLI, Git, AST, or formatters;
|
|
127
|
+
- every invalid shape produces a path-specific message such as `ignore[0].reason must be non-blank`.
|
|
128
|
+
|
|
129
|
+
### Phase 2 — immutable Git snapshot loader
|
|
130
|
+
|
|
131
|
+
**Files**
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
lib/jobcompat/git_repository.rb
|
|
135
|
+
lib/jobcompat/revision_snapshot.rb
|
|
136
|
+
test/support/temporary_repository.rb
|
|
137
|
+
test/integration/git_repository_test.rb
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Implementation tasks**
|
|
141
|
+
|
|
142
|
+
- implement a test helper that initializes a temporary repo, writes files, commits, and returns SHAs without network;
|
|
143
|
+
- locate the Git root from a nested directory;
|
|
144
|
+
- resolve refs with `--verify --end-of-options <ref>^{commit}`;
|
|
145
|
+
- enumerate default-format `ls-tree -r -z --full-tree` entries and split each NUL record at its first tab into mode/type/OID and path;
|
|
146
|
+
- keep regular tracked `.rb` path/OID metadata for the lazy presence-only pass, while filtering full analysis by config;
|
|
147
|
+
- implement the framed `cat-file --batch` reader;
|
|
148
|
+
- yield `(path, oid, bytes)` rather than accumulating full source trees;
|
|
149
|
+
- expose requested ref, resolved SHA, and files-scanned count;
|
|
150
|
+
- use `Open3` argv arrays and `GIT_OPTIONAL_LOCKS=0`.
|
|
151
|
+
|
|
152
|
+
**Tests**
|
|
153
|
+
|
|
154
|
+
- branch, tag, abbreviated/full SHA, and `HEAD` resolve to full SHA;
|
|
155
|
+
- invalid/missing/non-commit ref becomes `GitError`;
|
|
156
|
+
- base and head content differ without checkout;
|
|
157
|
+
- staged, unstaged, and untracked files are ignored;
|
|
158
|
+
- current branch/index/working-tree bytes and `git status --porcelain` are unchanged after reads;
|
|
159
|
+
- root and nested Ruby files read correctly;
|
|
160
|
+
- excluded blobs are not yielded;
|
|
161
|
+
- executable bit regular blob is included;
|
|
162
|
+
- symlink and submodule entries are skipped;
|
|
163
|
+
- paths containing spaces, tabs, Unicode, and newlines survive NUL enumeration;
|
|
164
|
+
- empty blobs and blobs without trailing newline frame correctly;
|
|
165
|
+
- missing/corrupt object or batch process failure is a Git error;
|
|
166
|
+
- same OID can be memoized without changing path/revision locations.
|
|
167
|
+
- added, deleted, and renamed paths are independently enumerated in each snapshot without diff or rename inference;
|
|
168
|
+
- excluded tracked `.rb` blobs remain available by OID for a candidate-only presence check, without entering normal `files_scanned` counts.
|
|
169
|
+
|
|
170
|
+
**Completion criteria**
|
|
171
|
+
|
|
172
|
+
- an integration test reads two commits while the user's checkout remains on head with dirty changes intact;
|
|
173
|
+
- one tree-list and one batch-content process per snapshot/comparison, not one process per file.
|
|
174
|
+
|
|
175
|
+
### Phase 3 — source locations and constant-name extraction
|
|
176
|
+
|
|
177
|
+
**Files**
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
lib/jobcompat/model/source_location.rb
|
|
181
|
+
lib/jobcompat/analysis/constant_name.rb
|
|
182
|
+
test/unit/constant_name_test.rb
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Implementation tasks**
|
|
186
|
+
|
|
187
|
+
- define immutable `SourceLocation` with 1-based line/byte-column;
|
|
188
|
+
- flatten `ConstantReadNode` and static `ConstantPathNode`;
|
|
189
|
+
- retain leading-root qualification;
|
|
190
|
+
- reject paths with dynamic parents;
|
|
191
|
+
- define lexical candidate generation for an unqualified constant.
|
|
192
|
+
|
|
193
|
+
**Tests**
|
|
194
|
+
|
|
195
|
+
- `ExportJob`, `Admin::ExportJob`, `::Admin::ExportJob`;
|
|
196
|
+
- nested paths with three segments;
|
|
197
|
+
- `self::ExportJob`, `factory::ExportJob`, and call-derived parents are non-static;
|
|
198
|
+
- lexical candidates for `Admin::Billing` are ordered `Admin::Billing::ExportJob`, `Admin::ExportJob`, `ExportJob`;
|
|
199
|
+
- root-qualified and already qualified names bypass lexical prefixes;
|
|
200
|
+
- Unicode constant names preserve bytes and locations;
|
|
201
|
+
- columns are converted exactly once from Prism's zero-based value.
|
|
202
|
+
|
|
203
|
+
**Completion criteria**
|
|
204
|
+
|
|
205
|
+
- no Ruby constant lookup or source execution occurs;
|
|
206
|
+
- constant utility accepts Prism nodes and returns a small value, not strings plus hidden flags.
|
|
207
|
+
|
|
208
|
+
### Phase 4 — worker discovery and arity model
|
|
209
|
+
|
|
210
|
+
**Files**
|
|
211
|
+
|
|
212
|
+
```text
|
|
213
|
+
lib/jobcompat/analysis/worker_discovery_visitor.rb
|
|
214
|
+
lib/jobcompat/analysis/defined_constant_index.rb
|
|
215
|
+
lib/jobcompat/model/worker_contract.rb
|
|
216
|
+
lib/jobcompat/analysis/sidekiq_analyzer.rb
|
|
217
|
+
test/analysis/worker_discovery_visitor_test.rb
|
|
218
|
+
test/unit/worker_contract_test.rb
|
|
219
|
+
test/analysis/sidekiq_analyzer_test.rb
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Implementation tasks**
|
|
223
|
+
|
|
224
|
+
- track module/class namespaces;
|
|
225
|
+
- collect direct `include` facts and direct instance `perform` definitions;
|
|
226
|
+
- group reopened class fragments per revision;
|
|
227
|
+
- limit v0.1 fragment conflicts to supported same-canonical Sidekiq worker fragments that cannot yield one deterministic direct positional `perform` contract; use the existing zero/multiple-`perform` reasons, without general Ruby class/module, superclass, constant, or load-order reconciliation;
|
|
228
|
+
- index exact/plausible static class/module declarations and statically named bindings separately from supported worker discovery;
|
|
229
|
+
- reuse selected-file ASTs for presence facts; lazily stream excluded tracked `.rb` blobs only for potential JC004/JC005 names, byte-prefilter by leaf token, and parse candidate-bearing blobs for presence only;
|
|
230
|
+
- cap unselected source bytes inspected at 64 MiB per snapshot; incomplete/ambiguous/invalid excluded source yields `unverified`, never an absence ERROR;
|
|
231
|
+
- calculate exact positional intervals;
|
|
232
|
+
- create a distinct unknown-contract object/reason;
|
|
233
|
+
- use only the fixed schema-v1 consumer unknown-reason enum;
|
|
234
|
+
- abort the snapshot on Prism parse errors;
|
|
235
|
+
- preserve a concise definition signature and source location.
|
|
236
|
+
|
|
237
|
+
**Worker-discovery tests**
|
|
238
|
+
|
|
239
|
+
| Case | Expected |
|
|
240
|
+
| --- | --- |
|
|
241
|
+
| `include Sidekiq::Job` | discovered |
|
|
242
|
+
| `include ::Sidekiq::Job` | discovered |
|
|
243
|
+
| `include(Sidekiq::Job)` | discovered |
|
|
244
|
+
| multi-argument include containing Sidekiq module | discovered |
|
|
245
|
+
| `include Sidekiq::Worker` | discovered legacy worker |
|
|
246
|
+
| nested `module Admin; class ExportJob` | `Admin::ExportJob` |
|
|
247
|
+
| `class Admin::ExportJob` | `Admin::ExportJob` |
|
|
248
|
+
| root-qualified class path | canonical name without leading `::` |
|
|
249
|
+
| unrooted multi-segment class path inside another module with direct Sidekiq include | JC007 with null worker, never mis-canonicalized |
|
|
250
|
+
| same leaf name in two namespaces | two independent workers |
|
|
251
|
+
| plain Ruby class | ignored |
|
|
252
|
+
| inherited base job | ignored |
|
|
253
|
+
| indirect concern | ignored |
|
|
254
|
+
| external `ExportJob.include` | ignored |
|
|
255
|
+
| `Class.new` | ignored |
|
|
256
|
+
| reopened worker: include and perform in separate files | one known contract |
|
|
257
|
+
| move only include fragment or only perform fragment between selected files | same canonical merged contract |
|
|
258
|
+
| rename one selected fragment file | same canonical merged contract |
|
|
259
|
+
| selected class remains but direct include changes to indirect concern | `defined_unrecognized`; JC007, never JC004 |
|
|
260
|
+
| class moves to excluded tracked `.rb` | `outside_scan_scope`; JC007, never JC004 |
|
|
261
|
+
| excluded candidate-bearing Ruby fails parse or presence budget exhausts | `unverified`; JC007, never JC004/JC005 |
|
|
262
|
+
| zero direct perform | unknown contract |
|
|
263
|
+
| two direct perform definitions | unknown duplicate contract |
|
|
264
|
+
| same canonical worker with direct `perform` definitions in two supported fragments | JC007 `multiple_perform_definitions` |
|
|
265
|
+
| a class/module kind disagreement or different superclass in another fragment | no conflict-specific JC007; existing supported evidence still applies |
|
|
266
|
+
| nested worker inside worker class | correct independent contexts |
|
|
267
|
+
|
|
268
|
+
**Arity tests**
|
|
269
|
+
|
|
270
|
+
| Signature | Expected |
|
|
271
|
+
| --- | --- |
|
|
272
|
+
| `def perform; end` | `0..0` |
|
|
273
|
+
| one/multiple required positional | exact count |
|
|
274
|
+
| one/multiple optional positional | continuous finite interval |
|
|
275
|
+
| required + optional | correct min/max |
|
|
276
|
+
| rest only | `0..∞` |
|
|
277
|
+
| required + rest | `required..∞` |
|
|
278
|
+
| required post after rest | min includes post |
|
|
279
|
+
| destructured required parameter | counts as one |
|
|
280
|
+
| block parameter | ignored for arity |
|
|
281
|
+
| name-only rename | equal contracts |
|
|
282
|
+
| `def perform(...)` | `0..∞`, forwarding |
|
|
283
|
+
| leading args plus forwarding | leading minimum, unbounded max |
|
|
284
|
+
| required/optional keyword | unknown |
|
|
285
|
+
| named keyword rest / `**nil` | unknown |
|
|
286
|
+
|
|
287
|
+
**Parse tests**
|
|
288
|
+
|
|
289
|
+
- valid file with Prism warnings still yields facts; warnings do not enter public findings or completed diagnostics;
|
|
290
|
+
- invalid Ruby gives all Prism error locations and no partial contracts;
|
|
291
|
+
- parse errors across multiple selected files are aggregated and deterministically sorted;
|
|
292
|
+
- source encoding error is a parse/tool error;
|
|
293
|
+
- same blob OID analysis reuse does not reuse wrong revision labels.
|
|
294
|
+
- a presence-only excluded-file parse failure does not become a global selected-file parse error.
|
|
295
|
+
|
|
296
|
+
**Completion criteria**
|
|
297
|
+
|
|
298
|
+
- `WorkerContract#accepts?` and `#superset_of?` pass exhaustive boundary tests;
|
|
299
|
+
- AST code contains no compatibility rule IDs.
|
|
300
|
+
- a presence-only result distinguishes `absent` from defined, out-of-scope, and unverified without loading application code.
|
|
301
|
+
|
|
302
|
+
### Phase 5 — producer discovery and name resolution
|
|
303
|
+
|
|
304
|
+
**Files**
|
|
305
|
+
|
|
306
|
+
```text
|
|
307
|
+
lib/jobcompat/analysis/producer_discovery_visitor.rb
|
|
308
|
+
lib/jobcompat/analysis/semantic_evidence.rb
|
|
309
|
+
lib/jobcompat/model/enqueue_call.rb
|
|
310
|
+
test/analysis/producer_discovery_visitor_test.rb
|
|
311
|
+
test/analysis/sidekiq_analyzer_test.rb
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Implementation tasks**
|
|
315
|
+
|
|
316
|
+
- collect unresolved enqueue facts for supported target method names;
|
|
317
|
+
- count direct async payload arguments;
|
|
318
|
+
- remove the schedule argument for in/at;
|
|
319
|
+
- recognize one `.set(...).perform_async` chain;
|
|
320
|
+
- detect splat/forwarding/malformed schedule as unknown;
|
|
321
|
+
- capture dynamic receiver warnings;
|
|
322
|
+
- use only the fixed schema-v1 producer unknown-reason enum and its precedence;
|
|
323
|
+
- resolve static receivers only after the base/head worker union exists.
|
|
324
|
+
- preserve relevant Prism token sequence, enclosing statement and lexical scope for revision-free JC007 evidence fingerprints; assign deterministic occurrence ordinals within each same-expression group.
|
|
325
|
+
|
|
326
|
+
**Producer-call tests**
|
|
327
|
+
|
|
328
|
+
| Call | Expected payload |
|
|
329
|
+
| --- | ---: |
|
|
330
|
+
| `Job.perform_async(id)` | 1 |
|
|
331
|
+
| `Job.perform_async` | 0 |
|
|
332
|
+
| `Job.perform_async(id, {"x" => 1})` | 2 |
|
|
333
|
+
| `Job.perform_async([id, other])` | 1 |
|
|
334
|
+
| `Job.perform_async(id: 1)` | 1 Hash payload; type safety not assessed |
|
|
335
|
+
| `Job.perform_in(5, id)` | 1 |
|
|
336
|
+
| `Job.perform_in(5, id, "csv")` | 2 |
|
|
337
|
+
| `Job.perform_at(time)` | 0 |
|
|
338
|
+
| `Job.set(queue: :critical).perform_async(id)` | 1 |
|
|
339
|
+
| `Job.perform_async(*args)` | unknown, JC007 fact |
|
|
340
|
+
| `Job.perform_async(...)` | unknown, JC007 fact |
|
|
341
|
+
| `Job.perform_in(*args)` | unknown schedule/payload split |
|
|
342
|
+
| `Job.perform_in` | unknown malformed scheduled call |
|
|
343
|
+
| `job_class.perform_async(id)` | dynamic worker, unknown |
|
|
344
|
+
| `factory.job.perform_at(time, id)` | dynamic worker, unknown |
|
|
345
|
+
| `Job&.perform_async(id)` | unknown safe-navigation receiver |
|
|
346
|
+
| `Job.public_send(:perform_async, id)` | ignored |
|
|
347
|
+
| `Sidekiq::Client.push(...)` | ignored/out of scope |
|
|
348
|
+
| `Job.perform_bulk(...)` | ignored/out of scope |
|
|
349
|
+
| `Job.set(...).perform_in(...)` | ignored/out of scope |
|
|
350
|
+
|
|
351
|
+
**Namespace-resolution tests**
|
|
352
|
+
|
|
353
|
+
- exact `Admin::ExportJob`;
|
|
354
|
+
- root `::Admin::ExportJob`;
|
|
355
|
+
- unqualified `ExportJob` inside `Admin` resolves to `Admin::ExportJob` when present;
|
|
356
|
+
- qualified `Admin::ExportJob` inside `Tenant` first tries `Tenant::Admin::ExportJob`, then top-level;
|
|
357
|
+
- fallback to top-level when namespaced worker absent;
|
|
358
|
+
- nested `module A::B` uses exact syntactic lexical scope;
|
|
359
|
+
- same leaf in two namespaces resolves by innermost candidate;
|
|
360
|
+
- constant receiver not in base/head worker union is ignored;
|
|
361
|
+
- head call to base-only removed worker remains attributable;
|
|
362
|
+
- head call to head-only new worker resolves;
|
|
363
|
+
- dynamic/unsupported parent never guesses.
|
|
364
|
+
|
|
365
|
+
**Completion criteria**
|
|
366
|
+
|
|
367
|
+
- producer visitor output is revision-independent unresolved data;
|
|
368
|
+
- a call with any splat cannot accidentally receive a numeric arity;
|
|
369
|
+
- Hash/Array AST contents are never recursively counted as multiple payload slots.
|
|
370
|
+
- repeated identical expressions in one scope retain separate occurrence identities, while an unchanged expression in base/head can pair.
|
|
371
|
+
|
|
372
|
+
### Phase 6 — pure compatibility matrix
|
|
373
|
+
|
|
374
|
+
**Files**
|
|
375
|
+
|
|
376
|
+
```text
|
|
377
|
+
lib/jobcompat/model/compatibility_result.rb
|
|
378
|
+
lib/jobcompat/compatibility/engine.rb
|
|
379
|
+
test/unit/compatibility_engine_test.rb
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Implementation tasks**
|
|
383
|
+
|
|
384
|
+
- group producers by revision/worker;
|
|
385
|
+
- compute known unique arities and unknown counts;
|
|
386
|
+
- calculate the four matrix cells with `fail > unknown > pass > not_applicable`;
|
|
387
|
+
- keep base/base as non-finding baseline context;
|
|
388
|
+
- return immutable per-worker results sorted by canonical name.
|
|
389
|
+
|
|
390
|
+
**Tests**
|
|
391
|
+
|
|
392
|
+
- no producers -> not applicable;
|
|
393
|
+
- all known arities accepted -> pass;
|
|
394
|
+
- one of several arities rejected -> fail;
|
|
395
|
+
- accepted known call plus unknown call -> unknown;
|
|
396
|
+
- rejected call plus unknown call -> fail;
|
|
397
|
+
- unknown consumer with producer -> unknown;
|
|
398
|
+
- missing consumer status is not silently pass;
|
|
399
|
+
- base/head producer sets remain distinct;
|
|
400
|
+
- zero-arity producers behave correctly;
|
|
401
|
+
- unbounded consumers accept arbitrarily large test arities;
|
|
402
|
+
- finite interval boundary inclusions/exclusions;
|
|
403
|
+
- matrix exactly reflects the motivating optional-argument scenario.
|
|
404
|
+
|
|
405
|
+
**Completion criteria**
|
|
406
|
+
|
|
407
|
+
- no I/O, parser objects, config, or output strings in the engine tests;
|
|
408
|
+
- table-driven tests cover every status transition.
|
|
409
|
+
|
|
410
|
+
### Phase 7 — rules, precedence, aggregation, suppression
|
|
411
|
+
|
|
412
|
+
**Files**
|
|
413
|
+
|
|
414
|
+
```text
|
|
415
|
+
lib/jobcompat/model/finding.rb
|
|
416
|
+
lib/jobcompat/compatibility/rules.rb
|
|
417
|
+
lib/jobcompat/compatibility/engine.rb
|
|
418
|
+
test/unit/rules_test.rb
|
|
419
|
+
test/unit/compatibility_engine_test.rb
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**Implementation tasks**
|
|
423
|
+
|
|
424
|
+
- produce pure engine preflight requests for potential JC004/JC005 names; after the CLI resolves opposite-snapshot `DefinedConstantIndex` statuses, emit absence ERROR only for `absent` and JC007 for present/unverified candidates;
|
|
425
|
+
- implement JC001 before JC003/JC002; a qualifying JC001 absorbs the same worker/arity's head-to-head mismatch as a second direction and includes both revisions' producer locations;
|
|
426
|
+
- implement JC006 interval narrowing;
|
|
427
|
+
- map unknown facts/contracts/presence transitions to JC007;
|
|
428
|
+
- derive JC007 direction and revision sets from the evidence role, then pair exact revision-free semantic fingerprints one-to-one across snapshots;
|
|
429
|
+
- implement evidence ownership and duplicate suppression;
|
|
430
|
+
- aggregate producer and consumer proof locations by rule/worker/arity; union directions/revisions without using them as aggregation keys;
|
|
431
|
+
- apply targeted config ignores and count suppressed findings;
|
|
432
|
+
- derive exit 0/1 after suppression.
|
|
433
|
+
|
|
434
|
+
**Compatibility scenario tests**
|
|
435
|
+
|
|
436
|
+
| Base | Head | Expected |
|
|
437
|
+
| --- | --- | --- |
|
|
438
|
+
| required arg added; base call used old arity | new rejects old | JC001 error |
|
|
439
|
+
| optional arg added, producer unchanged | broadening only | no finding |
|
|
440
|
+
| optional arg added and head starts 2-arg enqueue | base rejects, head accepts | JC002 error |
|
|
441
|
+
| positional arg removed and base producer uses removed arity | head rejects | JC001 error |
|
|
442
|
+
| optional becomes required, base has one-arg call | head rejects | JC001 error |
|
|
443
|
+
| optional becomes required, no base witness | contract narrowed | JC006 warning |
|
|
444
|
+
| finite interval narrows at max with no witness | narrowing | JC006 warning |
|
|
445
|
+
| worker declaration actually deleted from tracked `.rb` source | head presence proven absent | JC004 only |
|
|
446
|
+
| worker canonical class renamed in one change | old name proven absent; new name proven absent in base | JC004 plus conditional JC005 only when new enqueue exists |
|
|
447
|
+
| file renamed, canonical class/contract unchanged | both snapshots recognize same worker | no finding |
|
|
448
|
+
| worker moved to another selected file | same canonical worker | no finding |
|
|
449
|
+
| class moved outside normal scan scope to tracked `.rb` | head presence outside scan | JC007, not JC004 |
|
|
450
|
+
| direct Sidekiq include becomes indirect concern but class remains | head class defined, worker unrecognized | JC007, not JC004 |
|
|
451
|
+
| direct `perform` becomes unsupported keyword form | head worker contract unknown | JC007, not JC004 |
|
|
452
|
+
| new worker, no enqueue | class introduced only | no JC005 |
|
|
453
|
+
| new worker + head enqueue | base class proven absent; old process may consume queue | JC005 conditional-risk error |
|
|
454
|
+
| new head worker + enqueue, but base class remains unrecognized/outside scan | base absence not proven | JC007, not JC005 |
|
|
455
|
+
| new worker + splat enqueue | old fleet still lacks class | JC005 plus JC007 for head-to-head arity proof |
|
|
456
|
+
| head producer rejected by head, no qualifying base producer of same arity | current mismatch | JC003 error |
|
|
457
|
+
| head producer rejected by both base/head, no qualifying base producer of same arity | current mismatch | JC003 only |
|
|
458
|
+
| head producer accepted by base, rejected by head, no qualifying base producer of same arity | current mismatch | JC003 only |
|
|
459
|
+
| base producer rejected by both base/head | pre-existing mismatch | no JC001 |
|
|
460
|
+
| base producer accepted base, rejected head, and head emits same arity | shared head failure | one JC001 with `base_to_head` and `head_to_head`, no JC003 |
|
|
461
|
+
| no repository producer; head contract narrowed | queued history remains possible | JC006 warning, no queue-empty claim |
|
|
462
|
+
| worker contract unknown keywords | unproven | coalesced JC007 |
|
|
463
|
+
| base splat call | base-to-head unknown | JC007 warning |
|
|
464
|
+
| head splat call | head-to-base and head-to-head unknown | one JC007, two directions |
|
|
465
|
+
| dynamic call | worker null | JC007 warning |
|
|
466
|
+
| narrowing plus dynamic base call | distinct risks | JC006 + JC007 |
|
|
467
|
+
| parameter rename only | same interval | no finding |
|
|
468
|
+
| rest arg added | broadening | no finding |
|
|
469
|
+
| rest arg removed with witnessed large payload | regression | JC001 |
|
|
470
|
+
|
|
471
|
+
**De-duplication tests**
|
|
472
|
+
|
|
473
|
+
- multiple same-arity callsites aggregate into one JC001/JC002/JC003;
|
|
474
|
+
- different arities produce distinct findings when both incompatible;
|
|
475
|
+
- JC004 suppresses downstream missing-head errors only after completed absence proof;
|
|
476
|
+
- JC005 suppresses absent-base JC002 only after completed absence proof;
|
|
477
|
+
- JC001 owns the same worker/arity's head-to-head rejection and aggregates head producer locations; JC003 does not duplicate it;
|
|
478
|
+
- JC003 owns head-only mismatch and suppresses JC002 for that producer;
|
|
479
|
+
- JC001 prevents JC006 for the same proven narrowing;
|
|
480
|
+
- JC003 prevents JC006 when its current mismatch uses an arity accepted by base but removed by head;
|
|
481
|
+
- unchanged JC007 root in base/head yields one finding with `revisions: [base, head]`, unioned directions and both locations;
|
|
482
|
+
- different JC007 reasons, paths, scopes, expressions, or occurrence identities remain separate;
|
|
483
|
+
- an unchanged unsupported `perform` parameter list with an unrelated method-body edit retains one cross-revision JC007 root;
|
|
484
|
+
- two identical expressions in one scope produce two findings per revision and pair one-to-one only when group size and ordinals agree;
|
|
485
|
+
- changed duplicate multiplicity prevents ambiguous cross-revision pairing;
|
|
486
|
+
- one unknown AST node yields one JC007 even when two directions are affected;
|
|
487
|
+
- exact suppression removes all findings for that rule+worker but not another rule or namespace;
|
|
488
|
+
- warning-only and suppressed-error-only results exit 0;
|
|
489
|
+
- any remaining error exits 1.
|
|
490
|
+
|
|
491
|
+
**Completion criteria**
|
|
492
|
+
|
|
493
|
+
- all seven rule algorithms map line-by-line to `docs/spec-v0.1.md`;
|
|
494
|
+
- no rule relies on traversal or Hash insertion order;
|
|
495
|
+
- no JC004/JC005 is emitted from recognized-worker set difference alone.
|
|
496
|
+
|
|
497
|
+
### Phase 8 — text and JSON output
|
|
498
|
+
|
|
499
|
+
**Files**
|
|
500
|
+
|
|
501
|
+
```text
|
|
502
|
+
lib/jobcompat/formatter/text.rb
|
|
503
|
+
lib/jobcompat/formatter/json.rb
|
|
504
|
+
test/unit/text_formatter_test.rb
|
|
505
|
+
test/unit/json_formatter_test.rb
|
|
506
|
+
test/fixtures/output/*.txt
|
|
507
|
+
test/fixtures/output/*.json
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
**Implementation tasks**
|
|
511
|
+
|
|
512
|
+
- implement headers, comparison SHAs, deployment model, findings, risk, locations, remediation, and summary;
|
|
513
|
+
- implement `PASS` and `PASS WITH WARNINGS`;
|
|
514
|
+
- build JSON schema v1 completed and failed envelopes;
|
|
515
|
+
- include per-worker matrices;
|
|
516
|
+
- escape control characters and preserve UTF-8;
|
|
517
|
+
- enforce deterministic sorting before serialization;
|
|
518
|
+
- emit the fixed two-space pretty JSON shape plus exactly one terminal newline.
|
|
519
|
+
|
|
520
|
+
**Tests**
|
|
521
|
+
|
|
522
|
+
- exact golden text for each JC001–JC007;
|
|
523
|
+
- errors before warnings and stable rule/name/arity/location order;
|
|
524
|
+
- singular/plural summary grammar;
|
|
525
|
+
- no ANSI/control-sequence leakage;
|
|
526
|
+
- namespaced and Unicode paths display safely;
|
|
527
|
+
- completed JSON with error/warning/no finding/suppression;
|
|
528
|
+
- matched suppressions render deterministically in text and JSON without restoring suppressed findings;
|
|
529
|
+
- failed JSON for config, Git, and parse errors;
|
|
530
|
+
- every documented field present with null where unavailable;
|
|
531
|
+
- absent contracts serialize as null while unsupported contracts serialize as `status: unknown` objects;
|
|
532
|
+
- null contracts use `base_presence`/`head_presence` to distinguish class absence from unrecognized or out-of-scope declarations;
|
|
533
|
+
- `max_arity: null` for unbounded contracts;
|
|
534
|
+
- `revisions` and `directions` appear on every finding in canonical order; JC001 can carry both `base_to_head` and `head_to_head`;
|
|
535
|
+
- JC007 carries a fixed `unknown_reason`; an unchanged base/head root has one JSON finding with both revisions and all affected directions;
|
|
536
|
+
- JC004 and JC005 public text reflects static absence proof and JC005's conditional old-consumer risk;
|
|
537
|
+
- JC006 public text explicitly says missing repository producer evidence does not prove absence of queued, scheduled, retried, historical, or externally enqueued payloads;
|
|
538
|
+
- JSON parses and round-trips;
|
|
539
|
+
- exact bytes stable across repeated runs.
|
|
540
|
+
|
|
541
|
+
**Completion criteria**
|
|
542
|
+
|
|
543
|
+
- a reader can identify what changed, direction, risk, locations, and migration from the first finding without source lookup;
|
|
544
|
+
- formatters contain no interval membership or rule precedence logic.
|
|
545
|
+
|
|
546
|
+
### Phase 9 — end-to-end CLI orchestration
|
|
547
|
+
|
|
548
|
+
**Files**
|
|
549
|
+
|
|
550
|
+
```text
|
|
551
|
+
lib/jobcompat/cli.rb
|
|
552
|
+
test/integration/check_command_test.rb
|
|
553
|
+
test/integration/determinism_test.rb
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
**Implementation tasks**
|
|
557
|
+
|
|
558
|
+
- connect root/config/ref/snapshot/analyzer/engine/suppression/formatter;
|
|
559
|
+
- run the engine's pure presence-request preflight, resolve requested names through the lazy snapshot index, then call final pure rule evaluation;
|
|
560
|
+
- produce text and JSON failures according to stream policy;
|
|
561
|
+
- guarantee exit codes 0/1/2;
|
|
562
|
+
- avoid leaking backtraces for expected errors;
|
|
563
|
+
- permit running from nested directories;
|
|
564
|
+
- include requested refs and full resolved SHAs.
|
|
565
|
+
|
|
566
|
+
**Temporary-Git integration scenarios**
|
|
567
|
+
|
|
568
|
+
1. safe optional consumer expansion -> exit 0;
|
|
569
|
+
2. optional expansion plus head 2-arg producer -> JC002, exit 1;
|
|
570
|
+
3. required argument addition with base call -> JC001, exit 1;
|
|
571
|
+
4. head same-tree mismatch -> JC003, exit 1;
|
|
572
|
+
5. proven worker-class deletion from tracked `.rb` -> JC004, exit 1;
|
|
573
|
+
6. new worker and enqueue with proven base class absence -> conditional-risk JC005, exit 1;
|
|
574
|
+
7. narrowing without witness -> JC006, exit 0;
|
|
575
|
+
8. splat producer -> JC007, exit 0;
|
|
576
|
+
9. ignored JC005 -> exit 0 and suppressed count 1;
|
|
577
|
+
10. invalid base/head ref -> exit 2;
|
|
578
|
+
11. invalid config -> exit 2;
|
|
579
|
+
12. invalid Ruby in selected path -> exit 2;
|
|
580
|
+
13. invalid Ruby in excluded test path -> not parsed, normal result;
|
|
581
|
+
14. text and JSON outcomes contain the same finding counts/IDs;
|
|
582
|
+
15. invocation from a subdirectory finds root config;
|
|
583
|
+
16. explicit relative config resolves from invocation directory;
|
|
584
|
+
17. dirty working tree before/after bytes and status identical;
|
|
585
|
+
18. same base/head SHA still evaluates JC003 in that snapshot;
|
|
586
|
+
19. no network, Redis, Rails, or Sidekiq gem installed in the fixture.
|
|
587
|
+
|
|
588
|
+
**Required cross-revision integration matrix**
|
|
589
|
+
|
|
590
|
+
| Scenario | Expected result |
|
|
591
|
+
| --- | --- |
|
|
592
|
+
| same semantic `perform_async(*args)` JC007 root in base/head | one JC007, `revisions: [base, head]`, unioned directions, two revision-tagged locations |
|
|
593
|
+
| different JC007 roots by reason, source path, scope, expression, or occurrence | separate findings; no false merge |
|
|
594
|
+
| two identical unknown calls in one scope in both snapshots | two findings, paired by deterministic occurrence identity |
|
|
595
|
+
| base producer accepted by base and rejected by head, head producer same worker/arity also rejected by head | one JC001 with `base_to_head` and `head_to_head`, no JC003 |
|
|
596
|
+
| head producer mismatch without qualifying base evidence | standalone JC003 |
|
|
597
|
+
| actual deletion of base supported worker's class from head tracked `.rb` | JC004 error |
|
|
598
|
+
| direct include becomes indirect include, same class remains | JC007 warning, no JC004 |
|
|
599
|
+
| `perform` becomes unsupported keyword signature, same class remains | JC007 warning, no JC004 |
|
|
600
|
+
| file rename only, same canonical class and contract | no finding |
|
|
601
|
+
| class moved to another selected file | no finding |
|
|
602
|
+
| class moved to excluded or non-included tracked `.rb` | JC007 outside-analysis-scope warning, no JC004 |
|
|
603
|
+
| canonical class rename, old name proven absent | old name JC004 |
|
|
604
|
+
| canonical class rename plus new head enqueue and proven base absence of new name | old JC004 plus conditional-risk JC005 |
|
|
605
|
+
| include and `perform` in different selected files | one known merged worker contract |
|
|
606
|
+
| move only one reopened fragment or rename only one fragment file | same deterministic merged contract, no removal |
|
|
607
|
+
| move only the include fragment of a reopened class outside scan while `perform` remains selected | class still defined; JC007, no JC004 |
|
|
608
|
+
| move only the `perform` fragment outside scan while include remains selected | unknown contract JC007, no JC004 |
|
|
609
|
+
| excluded candidate-bearing blob cannot parse or presence scan hits 64 MiB budget | JC007 presence-unverified, no JC004/JC005 |
|
|
610
|
+
| new head worker enqueued but base class remains unrecognized or out of scan | JC007, no JC005 |
|
|
611
|
+
| new head worker with no head enqueue | no JC005, base presence `not_checked` in JSON |
|
|
612
|
+
| no repository producer and known worker contract narrows | JC006 warning; output does not infer queue emptiness |
|
|
613
|
+
|
|
614
|
+
**Determinism tests**
|
|
615
|
+
|
|
616
|
+
- create identical logical repositories with different file creation order and compare output;
|
|
617
|
+
- repeat command multiple times and compare bytes;
|
|
618
|
+
- callsites inserted in reverse lexical/tree order still sort identically;
|
|
619
|
+
- JSON object keys and arrays match golden files;
|
|
620
|
+
- summary counts remain consistent with findings/suppression.
|
|
621
|
+
- JC007 fingerprint and occurrence pairing are byte-stable across file creation/traversal order;
|
|
622
|
+
- one cross-revision JC007 warning counts once in text, JSON, and summary.
|
|
623
|
+
|
|
624
|
+
**Completion criteria**
|
|
625
|
+
|
|
626
|
+
- the executable passes a true subprocess test for each exit code and format;
|
|
627
|
+
- manual invocation on a fixture visibly matches the documented example.
|
|
628
|
+
|
|
629
|
+
### Phase 10 — README, CI, packaging, and release-readiness (no publish)
|
|
630
|
+
|
|
631
|
+
**Files**
|
|
632
|
+
|
|
633
|
+
```text
|
|
634
|
+
README.md
|
|
635
|
+
.github/workflows/ci.yml
|
|
636
|
+
jobcompat.gemspec
|
|
637
|
+
LICENSE
|
|
638
|
+
CHANGELOG.md # optional but recommended for public OSS
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
**Implementation tasks**
|
|
642
|
+
|
|
643
|
+
- write README in the exact outline from the spec;
|
|
644
|
+
- put the diff/error five-second example in the first screen;
|
|
645
|
+
- document native Sidekiq only and positional-arity-only limits;
|
|
646
|
+
- add the standard MIT license text for `2026 jobcompat contributors`;
|
|
647
|
+
- include installation, quick start, rules, config, CI, safety, and staged migration;
|
|
648
|
+
- add CI for supported Ruby 3.3, 3.4, and 4.0 using available current patch releases;
|
|
649
|
+
- run unit/integration suite and gem build in CI;
|
|
650
|
+
- verify gem contents include executable, lib, README, license, and docs but not temp files;
|
|
651
|
+
- add no release automation in v0.1 implementation unless separately requested.
|
|
652
|
+
|
|
653
|
+
**Tests/checks**
|
|
654
|
+
|
|
655
|
+
- all code blocks are syntactically coherent with the actual CLI;
|
|
656
|
+
- README command output matches formatter fixtures;
|
|
657
|
+
- `gem build` succeeds;
|
|
658
|
+
- install built gem into a temporary gem home and run help/version/check fixture;
|
|
659
|
+
- CI workflow does not require Redis or network beyond dependency installation;
|
|
660
|
+
- dependency/license metadata is correct;
|
|
661
|
+
- gem metadata omits repository/homepage URLs until a real public URL exists;
|
|
662
|
+
- `rg` confirms no README claim of ActiveJob/BullMQ/Celery support;
|
|
663
|
+
- link check where practical, with network failures reported rather than guessed.
|
|
664
|
+
|
|
665
|
+
**Completion criteria**
|
|
666
|
+
|
|
667
|
+
- a new user can understand value, install, run, interpret failure, and suppress a gated false positive from README alone;
|
|
668
|
+
- no commit, push, tag, release, or RubyGems publish occurs without a separate user request.
|
|
669
|
+
|
|
670
|
+
## 4. Concrete test inventory
|
|
671
|
+
|
|
672
|
+
The phase tests above are normative. The following file-level inventory prevents coverage gaps.
|
|
673
|
+
|
|
674
|
+
### `worker_discovery_visitor_test.rb`
|
|
675
|
+
|
|
676
|
+
- modern/legacy includes;
|
|
677
|
+
- top-level, nested modules, explicit class paths, root paths;
|
|
678
|
+
- multiple includes and parentheses;
|
|
679
|
+
- non-worker exclusions;
|
|
680
|
+
- inherited/concern/dynamic limitations;
|
|
681
|
+
- reopened classes;
|
|
682
|
+
- same leaf name under separate namespaces;
|
|
683
|
+
- nested context restoration;
|
|
684
|
+
- zero/multiple perform definitions.
|
|
685
|
+
- presence index exact/ambiguous class names, static named bindings, and selected/out-of-scope locations;
|
|
686
|
+
- deterministic fragment merge when include/perform files move independently.
|
|
687
|
+
|
|
688
|
+
### `defined_constant_index_test.rb`
|
|
689
|
+
|
|
690
|
+
- normal selected AST reuse and lazy excluded-blob presence-only parsing;
|
|
691
|
+
- leaf-token prefilter does not treat a reference as a definition;
|
|
692
|
+
- exact present, unrecognized, outside-scope, absent, and unverified outcomes;
|
|
693
|
+
- ambiguous paths, excluded parse/encoding failure, and 64 MiB budget block absence ERROR;
|
|
694
|
+
- presence-only source does not add producer facts or normal scan counts.
|
|
695
|
+
|
|
696
|
+
### `worker_contract_test.rb`
|
|
697
|
+
|
|
698
|
+
- min/max for every positional signature form;
|
|
699
|
+
- unbounded `nil` representation;
|
|
700
|
+
- accepts below/min/inside/max/above;
|
|
701
|
+
- interval superset finite/finite, finite/unbounded, unbounded/finite, unbounded/unbounded;
|
|
702
|
+
- parameter-name independence;
|
|
703
|
+
- unsupported keywords are not fake ranges.
|
|
704
|
+
|
|
705
|
+
### `producer_discovery_visitor_test.rb`
|
|
706
|
+
|
|
707
|
+
- async/in/at/set syntax;
|
|
708
|
+
- zero/hash/array/keyword-hash payload slots;
|
|
709
|
+
- nested namespaces and root qualification;
|
|
710
|
+
- splat/forwarding/malformed schedule;
|
|
711
|
+
- dynamic/safe-navigation receivers;
|
|
712
|
+
- ignored low-level/bulk/metaprogrammed APIs;
|
|
713
|
+
- source locations at the outer enqueue call.
|
|
714
|
+
- semantic fingerprint retains kind, reason, worker, path, lexical scope, normalized expression/statement, group size, and ordinal while excluding revision and line/column.
|
|
715
|
+
|
|
716
|
+
### `compatibility_engine_test.rb`
|
|
717
|
+
|
|
718
|
+
- all four matrix cells and status precedence;
|
|
719
|
+
- all A/B/C directions;
|
|
720
|
+
- optional-argument safe/unsafe rollout split;
|
|
721
|
+
- pre-existing mismatch exclusion;
|
|
722
|
+
- missing/new consumer states;
|
|
723
|
+
- known and unknown calls in combination.
|
|
724
|
+
|
|
725
|
+
### `rules_test.rb`
|
|
726
|
+
|
|
727
|
+
- one focused test per precondition branch for JC001–JC007;
|
|
728
|
+
- examples from the formal spec;
|
|
729
|
+
- false-positive-oriented cases such as feature-flag suppression;
|
|
730
|
+
- aggregation and precedence;
|
|
731
|
+
- exact suppression behavior;
|
|
732
|
+
- title/severity/directions/remediation public values.
|
|
733
|
+
- JC004/JC005 each require opposite-snapshot `absent`, not merely a missing recognized worker;
|
|
734
|
+
- JC001 absorbs same worker/arity JC003 and carries both directions;
|
|
735
|
+
- JC007 pairs exact roots across revisions without merging distinct occurrences.
|
|
736
|
+
|
|
737
|
+
### `git_repository_test.rb`
|
|
738
|
+
|
|
739
|
+
- real object reads from base/head commits;
|
|
740
|
+
- path framing and mode filtering;
|
|
741
|
+
- ref safety and moving refs;
|
|
742
|
+
- missing objects/errors;
|
|
743
|
+
- non-mutation proof.
|
|
744
|
+
|
|
745
|
+
### `check_command_test.rb`
|
|
746
|
+
|
|
747
|
+
- exit 0/1/2;
|
|
748
|
+
- text/JSON;
|
|
749
|
+
- invalid ref/config/source;
|
|
750
|
+
- default and explicit config;
|
|
751
|
+
- warnings-only behavior;
|
|
752
|
+
- stdout/stderr contract;
|
|
753
|
+
- help/version.
|
|
754
|
+
|
|
755
|
+
### `determinism_test.rb`
|
|
756
|
+
|
|
757
|
+
- finding order;
|
|
758
|
+
- location order;
|
|
759
|
+
- worker matrix order;
|
|
760
|
+
- unique numeric arity order;
|
|
761
|
+
- JSON key/array order and newline;
|
|
762
|
+
- repeated byte equality;
|
|
763
|
+
- summary reconciliation.
|
|
764
|
+
|
|
765
|
+
## 5. Manual QA gate
|
|
766
|
+
|
|
767
|
+
After automated tests pass, perform this exact user flow in a temporary Git repository:
|
|
768
|
+
|
|
769
|
+
1. commit base with `ExportJob#perform(user_id)` and a one-argument producer;
|
|
770
|
+
2. commit head with `perform(user_id, format = nil)` and a two-argument producer;
|
|
771
|
+
3. make an unrelated dirty working-tree edit;
|
|
772
|
+
4. run `jobcompat check --base <base-sha>`;
|
|
773
|
+
5. observe JC002, `HEAD producer -> base consumer`, remediation, base/head consumer plus head producer locations, and exit 1;
|
|
774
|
+
6. run JSON format and inspect parsed `schema_version`, matrix, finding, summary, and exit 1;
|
|
775
|
+
7. add a temporary `.jobcompat.yml` suppression with reason and observe exit 0, suppressed count, and the matched suppression audit record;
|
|
776
|
+
8. run `git status --porcelain` and byte-compare the dirty file before/after;
|
|
777
|
+
9. replace explicit head call with splat and observe JC007 warning plus exit 0;
|
|
778
|
+
10. corrupt one selected Ruby file in a commit and observe parse diagnostic plus exit 2.
|
|
779
|
+
|
|
780
|
+
This flow is required because unit tests alone do not prove the installed CLI, Git protocol, streams, or visible UX.
|
|
781
|
+
|
|
782
|
+
## 6. Documentation consistency review
|
|
783
|
+
|
|
784
|
+
Before declaring v0.1 complete, compare implementation and README against all four planning documents. Resolve these checks explicitly:
|
|
785
|
+
|
|
786
|
+
1. ERROR conditions use supported, deterministic set/range evidence.
|
|
787
|
+
2. Base-to-head and head-to-base are both exercised by integration tests.
|
|
788
|
+
3. Optional argument addition alone passes; starting to produce it triggers JC002.
|
|
789
|
+
4. Proven class absence triggers JC004 only; a defined, excluded, or unverified class yields JC007 instead.
|
|
790
|
+
5. New worker plus enqueue triggers JC005 only with proven base absence; class addition alone does not, and its risk text is conditional on old queue consumption.
|
|
791
|
+
6. Splat/dynamic/keyword cases are unknown, never pass.
|
|
792
|
+
7. No ActiveJob or other framework code entered v0.1.
|
|
793
|
+
8. The codebase still reflects one parser and one framework, without an adapter platform.
|
|
794
|
+
9. AST extraction has no rule decisions; engine has no Prism nodes.
|
|
795
|
+
10. Tests specify all public behavior without relying on network or Redis.
|
|
796
|
+
11. README's first screen contains the five-second value example.
|
|
797
|
+
12. Competitive claims use “none found in bounded research,” not absolute absence.
|
|
798
|
+
13. An unchanged JC007 root reports once across base/head, while distinct roots remain distinct.
|
|
799
|
+
14. A shared old/current mismatch is one multi-direction JC001; head-only mismatch is JC003.
|
|
800
|
+
15. File rename and selected reopened-fragment movement do not imply class removal.
|
|
801
|
+
16. No producer callsite is treated as proof that queues, retries, scheduled, or historical payloads are empty.
|
|
802
|
+
17. JC004/JC005 absence proofs use the bounded tracked-`.rb` presence pass; unverified results never become ERROR.
|
|
803
|
+
|
|
804
|
+
## 7. Recommended first implementation action
|
|
805
|
+
|
|
806
|
+
Start by selecting/installing a supported Ruby 3.3+ toolchain and implementing **Phase 0** only. Verify the gem builds and `jobcompat --help/--version/check --help` work before adding Git or AST behavior. The next increment should be strict config, then the immutable Git loader; this order gives every later parser/rule test a trustworthy input boundary.
|