jobcompat 0.1.0 → 0.2.1
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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +27 -10
- data/docs/architecture.md +17 -12
- data/docs/implementation-plan-v0.2.1.md +17 -0
- data/docs/implementation-plan-v0.2.md +29 -0
- data/docs/release-checklist-v0.1.0.md +4 -4
- data/docs/release-checklist-v0.2.0.md +61 -0
- data/docs/release-checklist-v0.2.1.md +76 -0
- data/docs/release-notes-v0.2.0.md +71 -0
- data/docs/release-notes-v0.2.1.md +61 -0
- data/docs/spec-v0.2.1.md +65 -0
- data/docs/spec-v0.2.md +67 -0
- data/lib/jobcompat/analysis.rb +143 -6
- data/lib/jobcompat/canonical_worker_name.rb +35 -0
- data/lib/jobcompat/cli.rb +1 -1
- data/lib/jobcompat/config.rb +2 -6
- data/lib/jobcompat/engine.rb +6 -3
- data/lib/jobcompat/version.rb +1 -1
- data/lib/jobcompat.rb +1 -0
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 47c2a0eafb9ae4ced87a7e13389e1ebd043a281d5b711f79bf668179592fea33
|
|
4
|
+
data.tar.gz: a2447a5027360b6bf7843c6c7766b6b8af1977a1e8308700d298e956b428d87e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f6d44307647aed53748ca8b429ecf7469bb79fac73644a9f559a4b7c89c97bb6c52d60da03edd604b787e4959366e775579fa7a140341f1153902eefda90f528
|
|
7
|
+
data.tar.gz: e338de388c16b907edcd05377460bbb741bc268033821a952e0e1ce65e49cc46bbe45348ca30414e65faddb3d9c7d179e15c5538c92373a07ae76e294c787f31
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1 - 2026-10-04
|
|
4
|
+
|
|
5
|
+
Canonical worker-name consistency:
|
|
6
|
+
|
|
7
|
+
- Accept canonical Unicode worker names, such as names with combining marks and qualified `Admin::ÁJob`, in targeted `.jobcompat.yml` suppressions. Matching stays exact and is not Unicode-normalized.
|
|
8
|
+
- Share one Prism-based canonical worker-name validator between Client String producer analysis and Config.
|
|
9
|
+
- Report malformed, non-constant, or invalid-byte `ignore[].worker` names as normal config errors (exit 2). Names that are not Ruby constants, such as `管理::輸出`, were previously accepted but could never match a finding.
|
|
10
|
+
|
|
11
|
+
## 0.2.0 - 2026-10-02
|
|
12
|
+
|
|
13
|
+
Broader native Sidekiq producer coverage:
|
|
14
|
+
|
|
15
|
+
- Recognize `perform_bulk`, Setter `perform_in` / `perform_at` / `perform_bulk`, and `Sidekiq::Client.push` / `push_bulk`.
|
|
16
|
+
- Normalize mixed bulk arities into distinct producer facts while retaining unknown rows.
|
|
17
|
+
- Emit JSON schema version 2 with producer uncertainty reasons for bulk and Client payloads.
|
|
18
|
+
- Resolve Client Hash splats and dynamic keys per key in source order, preserving known workers when only `"args"` is uncertain and preventing false JC005 when a later entry may replace `"class"`.
|
|
19
|
+
- Validate Client class Strings with Prism's Ruby constant syntax, preserving Unicode identities and reporting malformed or invalid-byte names as JC007.
|
|
20
|
+
|
|
3
21
|
## 0.1.0 - 2026-09-26
|
|
4
22
|
|
|
5
23
|
- Extract positional `perform` contracts from native `Sidekiq::Job` and `Sidekiq::Worker` classes, including reopened classes.
|
data/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Sidekiq persists a worker `class` and an `args` array. The worker receives those
|
|
|
27
27
|
|
|
28
28
|
## Installation
|
|
29
29
|
|
|
30
|
-
Ruby 3.3 or newer is required.
|
|
30
|
+
Ruby 3.3 or newer is required. Install with `gem install jobcompat`, or add `gem "jobcompat", require: false` to your Gemfile, run `bundle install`, and use `bundle exec jobcompat`. `prism` is the only runtime dependency; jobcompat does not require the Sidekiq gem to inspect source.
|
|
31
31
|
|
|
32
32
|
## Quick Start
|
|
33
33
|
|
|
@@ -54,11 +54,11 @@ ERROR JC001 ExportJob
|
|
|
54
54
|
Risk: Jobs queued by the base revision may fail after deployment.
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
The second direction appears only when HEAD also has a one-argument producer. Output includes source locations, suggested migration steps, resolved commit SHAs, and a summary. JSON output uses `schema_version:
|
|
57
|
+
The second direction appears only when HEAD also has a one-argument producer. Output includes source locations, suggested migration steps, resolved commit SHAs, and a summary. JSON output uses `schema_version: 2` and includes per-worker compatibility matrices.
|
|
58
58
|
|
|
59
59
|
## How it works
|
|
60
60
|
|
|
61
|
-
jobcompat reads committed Ruby blobs through Git, parses each selected file with Prism, groups reopened class fragments by canonical name,
|
|
61
|
+
jobcompat reads committed Ruby blobs through Git, parses each selected file with Prism, groups reopened class fragments by canonical name, normalizes supported Sidekiq producer syntax into worker/arity facts, then compares positional-arity intervals. Application code is never loaded or executed.
|
|
62
62
|
|
|
63
63
|
Its separate `DefinedConstantIndex` checks tracked Ruby class declarations and named bindings before declaring a worker class absent. A class that remains in an excluded file or stops using a direct Sidekiq include produces a warning rather than a removal error.
|
|
64
64
|
|
|
@@ -111,7 +111,7 @@ ignore:
|
|
|
111
111
|
reason: "Enqueue starts after deployment completes"
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
Each configured `include` or `exclude` list **replaces** its default list, so copy the defaults you still want. `--config PATH` selects another YAML file; a relative path is resolved from the invocation directory. Unknown keys and unsafe YAML aliases fail with exit 2. An ignore entry must have an exact rule, canonical worker name, and non-blank reason. Matched suppressions appear in text and JSON audit output.
|
|
114
|
+
Each configured `include` or `exclude` list **replaces** its default list, so copy the defaults you still want. `--config PATH` selects another YAML file; a relative path is resolved from the invocation directory. Unknown keys and unsafe YAML aliases fail with exit 2. An ignore entry must have an exact rule, canonical worker name, and non-blank reason. `ignore[].worker` uses the same canonical Ruby constant-name rules as exact Client String worker targets, including valid Unicode identifiers; matching is exact and not Unicode-normalized. Matched suppressions appear in text and JSON audit output.
|
|
115
115
|
|
|
116
116
|
## CI usage
|
|
117
117
|
|
|
@@ -119,13 +119,30 @@ Run `jobcompat check --base origin/main --format json` after fetching the compar
|
|
|
119
119
|
|
|
120
120
|
## Supported Sidekiq patterns
|
|
121
121
|
|
|
122
|
-
Direct `include Sidekiq::Job` and legacy `include Sidekiq::Worker` are recognized in top-level and supported namespaced classes. Reopened fragments may split the include and `perform` across files.
|
|
122
|
+
Direct `include Sidekiq::Job` and legacy `include Sidekiq::Worker` are recognized in top-level and supported namespaced classes. Reopened fragments may split the include and `perform` across files. Supported producer APIs are:
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
- `Job.perform_async(...)`, `Job.perform_in(schedule, ...)`, `Job.perform_at(time, ...)`, and `Job.perform_bulk(...)`;
|
|
125
|
+
- `Job.set(...).perform_async(...)`, `.perform_in(...)`, `.perform_at(...)`, and `.perform_bulk(...)`;
|
|
126
|
+
- `Sidekiq::Client.push(...)` and `Sidekiq::Client.push_bulk(...)` with a static Hash payload using string `"class"` and `"args"` keys.
|
|
125
127
|
|
|
126
|
-
|
|
128
|
+
Client Hash entries are resolved per key in source order. A later explicit `"class"` or `"args"` overrides earlier writes; a later unknown Hash splat or dynamic key makes only the keys it may replace uncertain. Static unrelated keys do not affect certainty. A statically known worker is preserved when only args may be replaced; an uncertain class is not attributed to an earlier worker and cannot produce JC005 for it.
|
|
127
129
|
|
|
128
|
-
|
|
130
|
+
Hash and Array expressions each count as one ordinary payload argument. Bulk calls produce one fact per distinct statically known inner Array arity:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
ExportJob.perform_bulk([
|
|
134
|
+
[1],
|
|
135
|
+
[2, "csv"]
|
|
136
|
+
])
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
jobcompat sees payload arities 1 and 2. A dynamic bulk row is retained as `dynamic_bulk_arguments` uncertainty without discarding known rows from the same callsite. A Client class String is an exact canonical Ruby constant name; it is never resolved relative to the surrounding Ruby namespace or Unicode-normalized. Malformed names produce JC007 warnings.
|
|
140
|
+
|
|
141
|
+
Positional `perform` parameters may be required, optional, rest, post-rest, or forwarding. Keyword parameters make the worker contract unknown in v0.2.
|
|
142
|
+
|
|
143
|
+
## Limitations and v0.2 non-goals
|
|
144
|
+
|
|
145
|
+
v0.2 checks **positional arity only**. It does not check value types, Hash internals, keyword compatibility, or JSON serialization. It supports native Sidekiq, not ActiveJob. Dynamic bulk collections are warning/unknown, Client payload variables are not followed, and custom wrappers are not analyzed. ActiveJob, arbitrary Client instances, Hash internal schemas, aliases, inheritance, concerns, arbitrary metaprogramming, feature flags, and general Ruby class/module or superclass conflict analysis remain outside scope.
|
|
129
146
|
|
|
130
147
|
No repository producer callsite does **not** prove an empty queue: queued, scheduled, retried, historical, and external jobs may exist. JC004 requires a completed static absence proof across tracked `.rb` source. An excluded or unrecognized declaration warns. JC005 is a structural rolling-deploy error conditional on an old Sidekiq process being able to consume the new job's queue. Queue isolation and feature flags are not analyzed. Dynamic source may warn or be missed. Working-tree edits are ignored.
|
|
131
148
|
|
|
@@ -150,11 +167,11 @@ Analysis reads local Git objects only. It does not connect to Redis, boot Rails,
|
|
|
150
167
|
|
|
151
168
|
## Roadmap
|
|
152
169
|
|
|
153
|
-
Possible future work includes
|
|
170
|
+
Possible future work includes SARIF output and additional evidence sources. Other frameworks are not supported now.
|
|
154
171
|
|
|
155
172
|
## Contributing
|
|
156
173
|
|
|
157
|
-
Run `bundle exec rake test` and `gem build jobcompat.gemspec` with Ruby 3.3 or newer. Tests create temporary Git repositories and need no Redis or Sidekiq server.
|
|
174
|
+
Run `bundle exec rake test` and `gem build jobcompat.gemspec` with Ruby 3.3 or newer. Tests create temporary Git repositories and need no Redis or Sidekiq server. The [v0.1 specification](docs/spec-v0.1.md) remains the baseline; [the normative v0.2 delta](docs/spec-v0.2.md) defines the added producer coverage, and [the v0.2.1 delta](docs/spec-v0.2.1.md) defines shared canonical worker-name validation.
|
|
158
175
|
|
|
159
176
|
## License
|
|
160
177
|
|
data/docs/architecture.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# jobcompat v0.
|
|
1
|
+
# jobcompat v0.2 architecture
|
|
2
2
|
|
|
3
|
-
Status:
|
|
4
|
-
Date: 2026-09-
|
|
3
|
+
Status: implemented design for `0.2.0`
|
|
4
|
+
Date: 2026-09-28
|
|
5
5
|
|
|
6
6
|
## 1. Architecture principles
|
|
7
7
|
|
|
@@ -9,7 +9,7 @@ Date: 2026-09-23
|
|
|
9
9
|
2. **No application execution:** parse source; never require, boot, or evaluate it.
|
|
10
10
|
3. **Extraction before semantics:** AST visitors produce facts; a pure engine compares facts.
|
|
11
11
|
4. **Conservative unknowns:** unsupported evidence is explicit and cannot become a pass.
|
|
12
|
-
5. **Small
|
|
12
|
+
5. **Small native Sidekiq surface:** one framework adapter, one parser, one command, two formatters.
|
|
13
13
|
6. **Determinism:** stable sorting and fully resolved commit SHAs are part of correctness.
|
|
14
14
|
7. **No speculative framework abstraction:** namespaces organize current responsibilities; no generic plugin API ships in v0.1.
|
|
15
15
|
|
|
@@ -187,10 +187,12 @@ It receives source bytes and a revision/path context. It does not open files, in
|
|
|
187
187
|
|
|
188
188
|
`WorkerDiscoveryVisitor` owns class/module namespace tracking, direct Sidekiq includes, and direct `perform` definitions.
|
|
189
189
|
|
|
190
|
-
`ProducerDiscoveryVisitor` owns calls
|
|
190
|
+
`ProducerDiscoveryVisitor` owns supported worker calls (`perform_async`, `perform_in`, `perform_at`, `perform_bulk`), supported `.set` chains, and exact static `Sidekiq::Client.push` / `push_bulk` calls. Syntax-specific extraction produces normalized producer facts before the compatibility engine. Bulk Array literals become one fact per distinct known inner arity plus at most the required unknown evidence; empty bulk Arrays produce no facts. Client class constants use lexical resolution, while static class Strings use exact canonical resolution. Client Hash entries are ordered evidence: for `"class"` and `"args"` independently, a shared right-to-left scan stops at the first entry capable of writing the key. An explicit string key proves its value; an unknown Hash splat or dynamic/non-static key prevents that proof; static unrelated keys are skipped. Known class evidence remains usable when only args are uncertain, while uncertain class evidence carries no worker attribution into the engine.
|
|
191
191
|
|
|
192
192
|
`ConstantName` is a small utility that flattens only `ConstantReadNode` and `ConstantPathNode`. It returns a value object with `segments`, `root_qualified`, and `static`; it does not resolve Ruby constants.
|
|
193
193
|
|
|
194
|
+
Client String targets use the internal `CanonicalWorkerName` validator shared by `push`, `push_bulk`, and `.jobcompat.yml` `ignore[].worker` validation (since v0.2.1). After safe UTF-8 conversion and encoding validation, Prism parses the value in isolation. Only a single `ConstantReadNode` or static `ConstantPathNode` chain is accepted. Root-qualified Strings are rejected, and the extracted canonical name must equal the complete original text, excluding whitespace, comments, and extra syntax. This preserves Ruby's Unicode identifiers without normalization or lexical prefixing. Expected encoding failures and invalid syntax become `unsupported_client_payload`; unexpected parser failures remain tool failures. No source is executed.
|
|
195
|
+
|
|
194
196
|
`DefinedConstantIndex` records exact or plausible canonical class/module declarations and statically named constant bindings, source locations, and normal-scan membership. It is distinct from supported Sidekiq worker recognition. For a name missing from one revision's recognized workers, it returns `recognized_worker`, `defined_unrecognized`, `outside_scan_scope`, `absent`, or `unverified` using the formal proof in the spec. A present-but-unrecognized class or incomplete presence pass blocks JC004/JC005 ERROR.
|
|
195
197
|
|
|
196
198
|
`SemanticEvidence` builds the revision-free JC007 fingerprint from uncertainty kind/reason, canonical worker when known, sorted relative paths, syntactic enclosing scope, trivia-free relevant token stream and enclosing statement, plus deterministic group size and occurrence ordinal. It retains every revision-tagged root location. Same-fingerprint roots pair one-to-one across snapshots; distinct roots remain separate.
|
|
@@ -212,7 +214,7 @@ The engine MUST be pure with respect to filesystem, Git, environment, clock, and
|
|
|
212
214
|
Formatters receive a completed or failed result envelope. They MUST NOT recalculate compatibility or suppression. They render already matched suppression audit records without reconstructing omitted findings.
|
|
213
215
|
|
|
214
216
|
- `Formatter::Text` renders concise human remediation.
|
|
215
|
-
- `Formatter::Json` constructs schema-version-
|
|
217
|
+
- `Formatter::Json` constructs schema-version-2 primitive Hash/Array data, preserving the v1 shape while extending producer `unknown_reason` values. It includes each finding's `revisions`, `directions`, and `unknown_reason` and each worker's base/head presence status, calls `JSON.pretty_generate`, and appends exactly one newline.
|
|
216
218
|
|
|
217
219
|
Both consume already sorted results.
|
|
218
220
|
|
|
@@ -317,6 +319,8 @@ Presence-only blobs reuse this parser with a narrower visitor. A candidate-beari
|
|
|
317
319
|
| keywords | `ParametersNode#keywords/#keyword_rest` | unsupported status, except forwarding node |
|
|
318
320
|
| forwarding | `ForwardingParameterNode` | unbounded positional acceptance |
|
|
319
321
|
| enqueue call | `CallNode#name/#receiver/#arguments/#block` | method, receiver fact, argument nodes, location |
|
|
322
|
+
| bulk rows | `ArrayNode#elements`, `SplatNode` | distinct known inner arities and dynamic-row evidence |
|
|
323
|
+
| Client payload | `HashNode` / `KeywordHashNode`, `AssocNode`, `AssocSplatNode` | exact string keys, worker target mode, args shape, dynamic payload evidence |
|
|
320
324
|
| safe navigation | `CallNode#call_operator_loc` whose source slice is `&.` | unknown receiver evidence |
|
|
321
325
|
| call arguments | `ArgumentsNode#arguments` | syntactic count |
|
|
322
326
|
| splat | `SplatNode` | unknown producer arity |
|
|
@@ -354,12 +358,13 @@ UnresolvedEnqueueCall
|
|
|
354
358
|
lexical_namespace Array<String>
|
|
355
359
|
payload_arity Integer | nil
|
|
356
360
|
arity_known true | false
|
|
357
|
-
method perform_async | perform_in | perform_at
|
|
361
|
+
method perform_async | perform_in | perform_at | perform_bulk | push | push_bulk
|
|
362
|
+
resolution_mode lexical_constant | client_constant | exact_string
|
|
358
363
|
location
|
|
359
364
|
unknown_reason Symbol | nil
|
|
360
365
|
```
|
|
361
366
|
|
|
362
|
-
The
|
|
367
|
+
The reason values map one-to-one to the frozen baseline strings in `docs/spec-v0.1.md` plus the schema-v2 producer additions in `docs/spec-v0.2.md`; visitors do not invent free-form reason text.
|
|
363
368
|
|
|
364
369
|
Only after both revisions' worker names are known does the engine resolve these facts. This avoids ordering dependence between files and enables head calls to removed base workers.
|
|
365
370
|
|
|
@@ -410,7 +415,7 @@ The JSON formatter serializes both classes as a contract object with `status: kn
|
|
|
410
415
|
worker_name : String | nil
|
|
411
416
|
payload_arity : Integer | nil
|
|
412
417
|
arity_known : Boolean
|
|
413
|
-
method : :perform_async | :perform_in | :perform_at
|
|
418
|
+
method : :perform_async | :perform_in | :perform_at | :perform_bulk | :push | :push_bulk
|
|
414
419
|
location : SourceLocation
|
|
415
420
|
unknown_reason : Symbol | nil
|
|
416
421
|
```
|
|
@@ -565,7 +570,7 @@ The v0.1 security posture is a product characteristic:
|
|
|
565
570
|
|
|
566
571
|
Threat boundary: the repository and config may be untrusted input. Prism and Git process those bytes, but jobcompat never evaluates Ruby. JSON/text escaping MUST prevent control characters in paths/excerpts from corrupting output framing. Text formatter should escape non-printable path characters; JSON handles them through the JSON encoder.
|
|
567
572
|
|
|
568
|
-
## 14. Future extension points without
|
|
573
|
+
## 14. Future extension points without over-engineering
|
|
569
574
|
|
|
570
575
|
The following seams are sufficient:
|
|
571
576
|
|
|
@@ -578,10 +583,10 @@ The following seams are sufficient:
|
|
|
578
583
|
|
|
579
584
|
Potential roadmap order:
|
|
580
585
|
|
|
581
|
-
1. more native Sidekiq producer forms
|
|
586
|
+
1. more native Sidekiq producer forms only after precision research;
|
|
582
587
|
2. SARIF/GitHub integration;
|
|
583
588
|
3. optional changed-path performance hints without weakening whole-snapshot facts;
|
|
584
589
|
4. ActiveJob as a separately specified adapter;
|
|
585
590
|
5. other ecosystems only after defining their serialization/deployment contracts.
|
|
586
591
|
|
|
587
|
-
Do not add an adapter registry, dependency injection container, generic AST facade, or rule plugin system
|
|
592
|
+
Do not add an adapter registry, dependency injection container, generic AST facade, or rule plugin system without a concrete second implementation.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# jobcompat v0.2.1 implementation plan
|
|
2
|
+
|
|
3
|
+
Status: implemented
|
|
4
|
+
|
|
5
|
+
## Delivery sequence
|
|
6
|
+
|
|
7
|
+
1. Extract the Prism-based Client String worker-name check into the internal `Jobcompat::CanonicalWorkerName.parse` (`lib/jobcompat/canonical_worker_name.rb`), which requires `prism` itself and depends on neither Analyzer nor Config.
|
|
8
|
+
2. Migrate `Analyzer#client_worker` to the shared validator, keeping `exact_string` and `unsupported_client_payload` behavior unchanged.
|
|
9
|
+
3. Migrate `Config.validate` `ignore[].worker` to the shared validator, removing the handwritten regex grammar and keeping the existing `ignore[N].worker is invalid` config error.
|
|
10
|
+
4. Add a focused validator unit matrix (accepted names, rejected syntax, invalid bytes, no normalization, standalone loading) and a producer/Config agreement check; extend Config unit tests for Unicode acceptance, invalid bytes, duplicates, and exact matching.
|
|
11
|
+
5. Add temporary-Git integration coverage for decomposed Unicode JC003 suppression with JSON and text audit, precomposed/decomposed exact-match guards, qualified Unicode names, non-canonical and invalid-byte config errors, and byte determinism.
|
|
12
|
+
6. Raise the gem version to 0.2.1 (JSON schema 2 and config version 1 unchanged) and synchronize README, CHANGELOG, architecture, and the normative delta.
|
|
13
|
+
7. Run the full suite on the supported Ruby matrix with Prism 1.9.0, syntax checks, gem build/contents/isolated install, CLI exit-code smoke scenarios, and diff self-review.
|
|
14
|
+
|
|
15
|
+
## Scope boundaries
|
|
16
|
+
|
|
17
|
+
No rule, finding field, uncertainty reason, config key, dependency, or release operation is added. Constant AST resolution, `DefinedConstantIndex`, the engine, formatter, and Git access are unchanged.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# jobcompat v0.2 implementation plan
|
|
2
|
+
|
|
3
|
+
Status: implemented
|
|
4
|
+
|
|
5
|
+
## Delivery sequence
|
|
6
|
+
|
|
7
|
+
1. Reconfirm the public Sidekiq Job/Setter and Client producer shapes from upstream source and documentation.
|
|
8
|
+
2. Extend syntax extraction while keeping the existing `Call` fact and compatibility engine semantics.
|
|
9
|
+
3. Share one pure bulk-arity extractor across worker, Setter, and Client bulk calls.
|
|
10
|
+
4. Add exact-string versus lexical-constant Client target resolution and explicit v2 uncertainty reasons.
|
|
11
|
+
5. Raise the gem version to 0.2.0 and JSON schema to 2 while leaving config version 1.
|
|
12
|
+
6. Add unit coverage for AST shapes, deduplication, mixed known/unknown rows, options, false positives, and 1,000-row fact bounds.
|
|
13
|
+
7. Add temporary-Git integration coverage for JC001, JC002, JC003, JC005, JC006 witness behavior, JC007, schema v2, and byte determinism.
|
|
14
|
+
8. Synchronize README, architecture, changelog, and this normative delta specification.
|
|
15
|
+
9. Run the full suite, syntax checks, gem build/contents/install, CLI and exit-code smoke scenarios, and diff review.
|
|
16
|
+
|
|
17
|
+
## Scope boundaries
|
|
18
|
+
|
|
19
|
+
The compatibility rules and their precedence are not changed. No Sidekiq dependency, dataflow, source execution, Client instance inference, wrapper inference, ActiveJob support, keyword compatibility, payload type/schema analysis, or release operation is added.
|
|
20
|
+
|
|
21
|
+
## Verification inventory
|
|
22
|
+
|
|
23
|
+
- Existing v0.1 suite remains green.
|
|
24
|
+
- Bulk extraction covers known, duplicate, zero-argument, empty, dynamic, mixed, splat, option, namespace, root-qualified, safe-navigation, and large-literal cases.
|
|
25
|
+
- Setter scheduled calls exclude their schedule argument.
|
|
26
|
+
- Client tests cover exact receivers, constant and String classes, empty and multi-argument payloads, options, dynamic/missing shapes, per-key final-write certainty for splats and dynamic keys, duplicate explicit keys, mixed splat/dynamic ordering, parentheses, and non-Sidekiq receivers. BASE/HEAD guards prove unknown class evidence cannot create JC005, while known class / unknown args still can, for both `push` and `push_bulk`.
|
|
27
|
+
- Client String validation reuses Prism's constant syntax and the existing constant extractor. Tests cover decomposed Unicode and qualified names, exact namespace matching, distinct precomposed/decomposed identities, non-canonical complete inputs, valid source encodings, and invalid bytes that warn without crashing. Both Client APIs exercise Unicode JC001/JC002/JC003/JC005 and deterministic Unicode/unsupported output.
|
|
28
|
+
- Integration tests prove new producer facts flow through JC001–JC007 without a new rule.
|
|
29
|
+
- Repeated text and JSON outputs are byte-identical.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# jobcompat v0.1.0 release checklist
|
|
2
2
|
|
|
3
|
-
This checklist
|
|
3
|
+
This checklist records preparation for the published v0.1.0 gem release. The public repository is `https://github.com/cottondesu/jobcompat`; gem version `0.1.0` uses tag `v0.1.0`; JSON `schema_version: 1` is a separate contract. Unchecked items remain external follow-ups and are not assertions that their gates were completed.
|
|
4
4
|
|
|
5
5
|
## Local, machine executable checks
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@ This checklist is for the future gem release. The public repository is `https://
|
|
|
13
13
|
- [x] Repeat the secret and personal path scan; review README, CHANGELOG, LICENSE, release notes, and workflow against the built gem.
|
|
14
14
|
- [x] Verify every GitHub Action reference is a pinned commit SHA and inspect the diff before the initial commit.
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## Pre-publication manual gates
|
|
17
17
|
|
|
18
18
|
- [x] Create the public [`cottondesu/jobcompat`](https://github.com/cottondesu/jobcompat) repository with default branch `main`.
|
|
19
19
|
- [x] Set the real repository URL in gemspec `homepage`, `source_code_uri`, `changelog_uri`, and `bug_tracker_uri`.
|
|
@@ -21,12 +21,12 @@ This checklist is for the future gem release. The public repository is `https://
|
|
|
21
21
|
- [ ] Configure a `main` ruleset or branch protection that blocks force pushes and deletion and requires successful CI; avoid unnecessary review gates for a single maintainer.
|
|
22
22
|
- [ ] Confirm GitHub two factor authentication and RubyGems MFA.
|
|
23
23
|
- [ ] Protect the GitHub `release` environment with appropriate approval or deployment restrictions.
|
|
24
|
-
- [ ] Confirm Ruby 3.3, 3.4, and 4.0 GitHub CI jobs all pass on the exact release candidate. Ruby 3.3 CI
|
|
24
|
+
- [ ] Confirm Ruby 3.3, 3.4, and 4.0 GitHub CI jobs all pass on the exact release candidate. Ruby 3.3 CI was mandatory because Ruby 3.3 was not tested locally during release preparation.
|
|
25
25
|
- [ ] Configure a RubyGems [Pending Trusted Publisher](https://guides.rubygems.org/trusted-publishing/) for the new `jobcompat` gem using GitHub owner `cottondesu`, repository `jobcompat`, workflow file `release.yml`, and environment `release`. Confirm the details match the release workflow. Do not store a long lived RubyGems API key in GitHub Secrets.
|
|
26
26
|
- [ ] Recheck that `jobcompat` is still unregistered on RubyGems immediately before the actual gem publication.
|
|
27
27
|
- [ ] Review the tag triggered release workflow and built gem one final time before pushing `v0.1.0`.
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## Historical publication sequence
|
|
30
30
|
|
|
31
31
|
1. Recheck gem name before every public push and again immediately before gem publication.
|
|
32
32
|
2. Rerun local tests, build, install, smoke checks, package review, and secret scan. Resolve every build warning.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# jobcompat v0.2.0 release checklist
|
|
2
|
+
|
|
3
|
+
This is preparation for an update to the already published `jobcompat` gem. Checked boxes record completed preparation checks; unchecked boxes are final release or post-publication gates, not claims of completion. No v0.2.0 tag or publication is authorized by this document.
|
|
4
|
+
|
|
5
|
+
## Release identity
|
|
6
|
+
|
|
7
|
+
- Repository: `cottondesu/jobcompat`, branch `main`.
|
|
8
|
+
- Gem version: `0.2.0`; intended annotated tag: `v0.2.0`.
|
|
9
|
+
- Intended release date: `2026-10-02`. If execution is delayed, reconcile the CHANGELOG date with the actual intended date before tagging; never backdate publication.
|
|
10
|
+
- JSON schema: `2`; `.jobcompat.yml` configuration schema: `1`.
|
|
11
|
+
- Implementation commit: `c107c9a9868cf3d29f4b821444fe1e0cfcb86820`.
|
|
12
|
+
- Release candidate commit: to be determined after the `Prepare v0.2.0 release` commit. Record its exact SHA and CI run in the final release execution record; do not select a moving `main` ref without verification.
|
|
13
|
+
|
|
14
|
+
## Automated validation
|
|
15
|
+
|
|
16
|
+
- [x] Implementation commit passed [CI run 36875203650](https://github.com/cottondesu/jobcompat/actions/runs/36875203650): Ruby 3.3, 3.4, and 4.0 PASS; each ran 122 tests / 902 assertions and built the Gem.
|
|
17
|
+
- [x] Candidate local `bundle exec rake test` on Ruby 3.4.11 / Prism 1.9.0: 122 tests, 902 assertions, zero failures, errors, and skips.
|
|
18
|
+
- [x] Candidate `git diff --check` and release-only diff review pass; product bytes match the implementation commit.
|
|
19
|
+
- [x] Build the candidate Gem and inspect its 24 entries, including both v0.2.0 release documents. No local tooling state, credentials, caches, scratch files, or nested Gems are included.
|
|
20
|
+
- [x] Install the candidate Gem into a fresh isolated destination; version/help, clean 0, compatibility error 1, warning-only 0, invalid ref 2, and config version 1 pass.
|
|
21
|
+
- [ ] Record the pushed release-preparation SHA and its CI run; require Ruby 3.3, 3.4, and 4.0 success on that exact SHA before tagging. The implementation run above does not substitute for candidate CI.
|
|
22
|
+
|
|
23
|
+
## Publication prerequisites
|
|
24
|
+
|
|
25
|
+
- [x] On 2026-10-02, public RubyGems listed `0.1.0` and no `0.2.0`; public owner handle was `cottondesu`, and metadata pointed to this repository.
|
|
26
|
+
- [x] On 2026-10-02, local/remote tags and GitHub Releases contained `v0.1.0` and no `v0.2.0`.
|
|
27
|
+
- [x] GitHub environment `release` exists, requires reviewer `cottondesu`, and permits tag pattern `v*`.
|
|
28
|
+
- [ ] Confirm the current RubyGems Trusted Publisher for `jobcompat` in the maintainer UI: owner `cottondesu`, repository `jobcompat`, workflow file `release.yml`, environment `release`. Public package metadata and earlier publication are not proof of the current publisher settings. See [Trusted Publishing](https://guides.rubygems.org/trusted-publishing/).
|
|
29
|
+
- [x] Release workflow retains tag/version and public metadata gates. Gem homepage, source, changelog, and issue URLs refer to `cottondesu/jobcompat`.
|
|
30
|
+
- [x] Review the v0.2.0 release notes against the normative scope and inspect the built candidate package.
|
|
31
|
+
- [ ] Immediately before release execution, recheck that `0.2.0` is absent on RubyGems and `v0.2.0` is absent locally, remotely, and in GitHub Releases. Stop if any already exists; never overwrite it.
|
|
32
|
+
|
|
33
|
+
## Tag gate
|
|
34
|
+
|
|
35
|
+
**Pushing `v0.2.0` triggers the Release workflow and may publish the Gem automatically. Treat tag push as a publication action.**
|
|
36
|
+
|
|
37
|
+
- [ ] Complete every pre-tag validation and publication prerequisite above, including the manual Trusted Publisher check and exact candidate CI.
|
|
38
|
+
- [ ] Obtain separate final release execution approval. Release preparation alone does not authorize tagging or publication.
|
|
39
|
+
- [ ] Confirm the exact release candidate SHA, product version, intended release date, and unchanged v0.1.0 release identity.
|
|
40
|
+
- [ ] Under that separate approval, create an annotated `v0.2.0` tag on the verified SHA and push only that tag. Do not force or push unrelated tags.
|
|
41
|
+
|
|
42
|
+
## Release workflow verification
|
|
43
|
+
|
|
44
|
+
The unchanged `.github/workflows/release.yml` triggers on pushes matching `v*`. Its Ruby 3.3 / 3.4 / 4.0 tests precede the `publish` job through `needs: test`. Publication uses Ruby 4.0, environment `release`, GitHub OIDC `id-token: write`, pinned `rubygems/configure-rubygems-credentials`, and `gem push`.
|
|
45
|
+
|
|
46
|
+
- [ ] Verify the triggered Release run refers to the approved tag and exact candidate SHA.
|
|
47
|
+
- [ ] Confirm all three test jobs pass and the publication environment approval is satisfied.
|
|
48
|
+
- [ ] Confirm `GITHUB_REF_NAME == "v#{Jobcompat::VERSION}"`, public metadata checks, publication-job tests, build, credentials setup, and push all succeed.
|
|
49
|
+
- [ ] Confirm RubyGems lists `jobcompat` version `0.2.0`; do not infer publication solely from a tag or a partial workflow result.
|
|
50
|
+
|
|
51
|
+
## After publication
|
|
52
|
+
|
|
53
|
+
- [ ] Check RubyGems version, ownership, Ruby/dependency requirements, public URLs, and package contents.
|
|
54
|
+
- [ ] Install `jobcompat` 0.2.0 from RubyGems in a clean environment; verify version/help and representative CLI checks.
|
|
55
|
+
- [ ] Under the separate release approval, create or verify the GitHub Release for `v0.2.0` using the reviewed release notes and correct tag.
|
|
56
|
+
- [ ] Record the release workflow URL and publication evidence; monitor issue/security reports.
|
|
57
|
+
- [ ] Track the pre-existing Unicode suppression validator limitation as a possible v0.2.1 follow-up; do not alter v0.2.0 bytes after publication.
|
|
58
|
+
|
|
59
|
+
## Rollback
|
|
60
|
+
|
|
61
|
+
Do not overwrite or reuse version `0.2.0` after publication, move the published tag, or alter v0.1.0. Fix forward in `0.2.1`. If necessary, use the official [RubyGems yanking procedure](https://guides.rubygems.org/removing-a-published-gem/) and communicate the affected version; yanking does not remove installed copies.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# jobcompat v0.2.1 release checklist
|
|
2
|
+
|
|
3
|
+
This is preparation for a maintenance update to the already published `jobcompat` gem. Checked boxes record completed preparation checks; unchecked boxes are final release or post-publication gates, not claims of completion. No v0.2.1 tag or publication is authorized by this document.
|
|
4
|
+
|
|
5
|
+
## Release identity
|
|
6
|
+
|
|
7
|
+
- Repository: `cottondesu/jobcompat`, branch `main`.
|
|
8
|
+
- Gem version: `0.2.1`; intended annotated tag: `v0.2.1`.
|
|
9
|
+
- Intended release date: `2026-10-04`. If execution is delayed, reconcile the CHANGELOG date with the actual intended date before tagging; never backdate publication.
|
|
10
|
+
- JSON schema: `2`; `.jobcompat.yml` configuration schema: `1`.
|
|
11
|
+
- Implementation commit: `fd6d138f229228be8801de9a7ae040cab21c9923` (`Fix Unicode worker suppression validation`), on top of the v0.2.0 release commit `378203731bcc0e4a06a1a3550fbd5605d9760f2e`.
|
|
12
|
+
- Implementation history note: the implementation was first pushed as `0397dc5b7b8eb4acaa975d8d3b628aa4f87aed8a`. At the maintainer's request, before any tag or publication, it was replaced once on `main` by `fd6d138` to correct the commit author and committer and remove a co-author trailer. Both commits have the identical tree `18602325cc75951aee1d078c92c4c7813443f570`, so the reviewed product bytes are unchanged. `0397dc5` is no longer on `main` and must not be tagged.
|
|
13
|
+
- Release candidate commit: to be determined after the `Prepare v0.2.1 release` commit. Record its exact SHA and CI run in the final release execution record; do not select a moving `main` ref without verification.
|
|
14
|
+
|
|
15
|
+
## Reviewed implementation
|
|
16
|
+
|
|
17
|
+
- [x] Fresh independent review of the complete v0.2.1 diff: verdict SHIP, no BLOCKER, no MAJOR.
|
|
18
|
+
- [x] Independent validation: 140 tests, 1298 assertions, 0 failures, 0 errors, 0 skips with Prism 1.9.0 under a UTF-8 locale, on Ruby 3.3.6 (reviewer environment), Ruby 3.4.8 (conda-forge build), and Ruby 4.0.6 (conda-forge build). Official GitHub CI below is the release authority.
|
|
19
|
+
- [x] Reviewer MINOR, test-only: under a POSIX / US-ASCII locale, the Unicode suppression text assertion in `test/integration/check_command_test.rb` raises `Encoding::CompatibilityError` in the test itself. Production output is byte-identical between UTF-8 and POSIX locales; no production locale regression was observed. Non-release-blocking; deferred past v0.2.1 and intentionally left unchanged in this release. Canonical local validation uses a UTF-8 locale.
|
|
20
|
+
|
|
21
|
+
## Automated validation
|
|
22
|
+
|
|
23
|
+
- [x] Implementation commit `fd6d138` passed [CI run 36992116824](https://github.com/cottondesu/jobcompat/actions/runs/36992116824): Ruby 3.3.12, 3.4.10, and 4.0.6 PASS; each ran 140 tests / 1298 assertions with 0 failures, errors, and skips and built `jobcompat-0.2.1.gem`. (Superseded commit `0397dc5` had passed [CI run 36990271965](https://github.com/cottondesu/jobcompat/actions/runs/36990271965) with the same tree.)
|
|
24
|
+
- [x] Candidate local `LANG=C.UTF-8 bundle exec rake test` on Ruby 3.3.6 / Prism 1.9.0: 140 tests, 1298 assertions, zero failures, errors, and skips.
|
|
25
|
+
- [x] Candidate `git diff --check` and release-only diff review pass; only `CHANGELOG.md` and the two v0.2.1 release documents change, and product bytes match the implementation commit.
|
|
26
|
+
- [x] Build the candidate Gem and inspect its entries, including `lib/jobcompat/canonical_worker_name.rb`, the v0.2.1 spec and implementation plan, and both v0.2.1 release documents. No local tooling state, credentials, caches, scratch files, patches, review files, or nested Gems are included.
|
|
27
|
+
- [x] Install the candidate Gem into a fresh isolated destination under a UTF-8 locale; `jobcompat --version` reports `jobcompat 0.2.1`; clean 0, compatibility error 1, warning-only 0, invalid config 2, invalid ref 2, and Unicode suppression 0 pass.
|
|
28
|
+
- [x] Release-note examples checked with the installed candidate and Prism 1.9.0: a `ÁJob` (`A` + `U+0301` + `Job`) suppression is valid, `管理::輸出` is a `config_error` with exit 2, and `Job管理::E輸出` is valid.
|
|
29
|
+
- [ ] Record the pushed release-preparation SHA and its CI run; require Ruby 3.3, 3.4, and 4.0 success, including the Gem build step, on that exact SHA before tagging. The implementation run above does not substitute for candidate CI.
|
|
30
|
+
|
|
31
|
+
## Compatibility note
|
|
32
|
+
|
|
33
|
+
- [x] The v0.2.1 release notes state prominently that v0.2.0 could silently accept `ignore[].worker` names that are not Ruby constant names, that such entries could never match a finding under the canonical worker-name semantics, and that v0.2.1 rejects them as `config_error` with exit 2.
|
|
34
|
+
- [x] The note explains why `管理::輸出` is rejected (not a Ruby constant path) and shows that CJK constant paths such as `Job管理::E輸出` remain accepted.
|
|
35
|
+
- [x] The v0.2.0 limitation about combining-mark suppressions is not repeated, because v0.2.1 fixes it.
|
|
36
|
+
|
|
37
|
+
## Publication prerequisites
|
|
38
|
+
|
|
39
|
+
- [x] On 2026-10-02, public RubyGems listed `0.1.0` and `0.2.0` and no `0.2.1`.
|
|
40
|
+
- [x] On 2026-10-02, local and remote tags were `v0.1.0` and `v0.2.0` only, and GitHub Releases contained `v0.1.0` and `v0.2.0` and no `v0.2.1`.
|
|
41
|
+
- [ ] Confirm GitHub environment `release` still exists, requires reviewer `cottondesu`, and permits tag pattern `v*`. The preparation tooling could not read environment settings, so this is a manual gate. Do not weaken these protections.
|
|
42
|
+
- [ ] Confirm the current RubyGems Trusted Publisher for `jobcompat` in the maintainer UI: owner `cottondesu`, repository `jobcompat`, workflow file `release.yml`, environment `release`. Public package metadata and earlier publications are not proof of the current publisher settings. Do not introduce a long-lived RubyGems API key. See [Trusted Publishing](https://guides.rubygems.org/trusted-publishing/).
|
|
43
|
+
- [x] Release workflow retains the tag/version and public metadata gates. Gem homepage, source, changelog, and issue URLs refer to `cottondesu/jobcompat`.
|
|
44
|
+
- [x] Review the v0.2.1 release notes against the normative scope and inspect the built candidate package.
|
|
45
|
+
- [x] On 2026-10-04, current-state recheck: public RubyGems listed `0.2.0` and `0.1.0` and no `0.2.1` (versions API 404); remote tags were `v0.1.0` and `v0.2.0` only, with no local `v0.2.1` tag; GitHub Releases were `v0.2.0` and `v0.1.0`. Repeat this check immediately before release execution.
|
|
46
|
+
- [ ] Immediately before release execution, recheck that `0.2.1` is absent on RubyGems and `v0.2.1` is absent locally, remotely, and in GitHub Releases. Stop if any already exists; never overwrite it.
|
|
47
|
+
|
|
48
|
+
## Tag gate
|
|
49
|
+
|
|
50
|
+
**Pushing `v0.2.1` triggers the Release workflow and may publish the Gem automatically. Treat tag push as a publication action.**
|
|
51
|
+
|
|
52
|
+
- [ ] Complete every pre-tag validation and publication prerequisite above, including the manual environment and Trusted Publisher checks and exact candidate CI.
|
|
53
|
+
- [ ] Obtain separate final release execution approval. Release preparation alone does not authorize tagging or publication.
|
|
54
|
+
- [ ] Confirm the exact release candidate SHA, product version, intended release date, and unchanged v0.1.0 and v0.2.0 release identities.
|
|
55
|
+
- [ ] Under that separate approval, create an annotated `v0.2.1` tag on the verified SHA and push only that tag. Do not force or push unrelated tags.
|
|
56
|
+
|
|
57
|
+
## Release workflow verification
|
|
58
|
+
|
|
59
|
+
The unchanged `.github/workflows/release.yml` triggers on pushes matching `v*`. Its Ruby 3.3 / 3.4 / 4.0 tests precede the `publish` job through `needs: test`. Publication uses Ruby 4.0, environment `release`, GitHub OIDC `id-token: write`, the tag/version and public metadata checks, pinned `rubygems/configure-rubygems-credentials`, and `gem push`.
|
|
60
|
+
|
|
61
|
+
- [ ] Verify the triggered Release run refers to the approved tag and exact candidate SHA.
|
|
62
|
+
- [ ] Confirm all three test jobs pass and the publication environment approval is satisfied.
|
|
63
|
+
- [ ] Confirm `GITHUB_REF_NAME == "v#{Jobcompat::VERSION}"`, public metadata checks, publication-job tests, build, credentials setup, and push all succeed.
|
|
64
|
+
- [ ] Confirm RubyGems lists `jobcompat` version `0.2.1`; do not infer publication solely from a tag or a partial workflow result.
|
|
65
|
+
|
|
66
|
+
## After publication
|
|
67
|
+
|
|
68
|
+
- [ ] Check RubyGems version, ownership, Ruby/dependency requirements, public URLs, and package contents.
|
|
69
|
+
- [ ] Install `jobcompat` 0.2.1 from RubyGems in a clean environment; verify version/help and representative CLI checks.
|
|
70
|
+
- [ ] Under the separate release approval, create or verify the GitHub Release for `v0.2.1` using the reviewed release notes and correct tag.
|
|
71
|
+
- [ ] Record the release workflow URL and publication evidence; monitor issue/security reports.
|
|
72
|
+
- [ ] Track the deferred test-only POSIX locale MINOR as a follow-up after v0.2.1; do not alter v0.2.1 bytes after publication.
|
|
73
|
+
|
|
74
|
+
## Rollback
|
|
75
|
+
|
|
76
|
+
Do not overwrite or reuse version `0.2.1` after publication, move the published tag, or alter v0.1.0 or v0.2.0. Fix forward in a later patch version. If necessary, use the official [RubyGems yanking procedure](https://guides.rubygems.org/removing-a-published-gem/) and communicate the affected version; yanking does not remove installed copies.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# jobcompat v0.2.0 release notes
|
|
2
|
+
|
|
3
|
+
jobcompat v0.2.0 broadens native Sidekiq producer coverage while preserving JC001–JC007, the three rolling-deployment directions, and positional-arity analysis.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
|
|
7
|
+
- Recognize worker bulk calls, scheduled and bulk Setter calls, and direct static Sidekiq Client payloads.
|
|
8
|
+
- Normalize bulk jobs into distinct arity facts without losing known rows when other rows are unknown.
|
|
9
|
+
- Resolve Client Hash evidence independently for `"class"` and `"args"` in source order.
|
|
10
|
+
- Validate exact String worker names with Prism's Ruby constant syntax, preserving Unicode spelling and handling malformed names conservatively.
|
|
11
|
+
- Emit JSON schema version 2; `.jobcompat.yml` remains version 1.
|
|
12
|
+
|
|
13
|
+
## New producer coverage
|
|
14
|
+
|
|
15
|
+
The following forms join the existing `perform_async`, `perform_in`, `perform_at`, and `set(...).perform_async` support:
|
|
16
|
+
|
|
17
|
+
- `Worker.perform_bulk(...)`;
|
|
18
|
+
- `Worker.set(...).perform_in(...)` and `.perform_at(...)`;
|
|
19
|
+
- `Worker.set(...).perform_bulk(...)`;
|
|
20
|
+
- `Sidekiq::Client.push(...)` and `.push_bulk(...)`.
|
|
21
|
+
|
|
22
|
+
Setter options and the first schedule argument of `perform_in` / `perform_at` do not count as job payload arguments.
|
|
23
|
+
|
|
24
|
+
## Bulk behavior
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
ExportJob.perform_bulk([
|
|
28
|
+
[1],
|
|
29
|
+
[2, "csv"]
|
|
30
|
+
])
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This supplies distinct positional arity facts for 1 and 2. Duplicate arities are collapsed, and an empty bulk Array supplies no producer evidence. Dynamic rows or inner splats remain conservative JC007 evidence without discarding known sibling rows. A dynamic outer collection remains unknown. Bulk calls use the existing compatibility rules; findings still carry one integer or null `payload_arity`.
|
|
34
|
+
|
|
35
|
+
## Sidekiq::Client support
|
|
36
|
+
|
|
37
|
+
Direct `Sidekiq::Client.push` and `push_bulk` calls accept statically understandable Hash payloads with string `"class"` and `"args"` keys. `push` counts a static args Array; `push_bulk` applies the same bulk normalization as worker calls.
|
|
38
|
+
|
|
39
|
+
For each relevant key, the last entry capable of writing it determines certainty. Later explicit keys can restore certainty after earlier splats or dynamic keys. Later unknown writes prevent proof of the affected key. A known worker with unknown args can still supply JC005 presence evidence and worker-attributed JC007; an uncertain class is not attributed to an earlier worker.
|
|
40
|
+
|
|
41
|
+
Class constants use the existing bounded lexical, qualified, and root-qualified resolution. Class Strings are exact canonical Ruby constant paths accepted by Prism, without leading `::`, namespace prefixing, or Unicode normalization. Malformed syntax and invalid String bytes become unsupported JC007 evidence. Payload variables, general Hash evaluation, Client instances, and wrappers are not inferred. See the [normative v0.2 scope](spec-v0.2.md) for supported options and uncertainty reasons.
|
|
42
|
+
|
|
43
|
+
## JSON schema 2
|
|
44
|
+
|
|
45
|
+
v0.1 emitted `schema_version: 1`; v0.2 emits `schema_version: 2`. Schema 2 extends producer uncertainty reasons for bulk and Client APIs while preserving the existing output structure, findings, directions, revisions, and locations.
|
|
46
|
+
|
|
47
|
+
The added reasons are `dynamic_bulk_arguments`, `dynamic_bulk_options`, `dynamic_client_payload`, `dynamic_client_class`, `dynamic_client_args`, and `unsupported_client_payload`.
|
|
48
|
+
|
|
49
|
+
The `.jobcompat.yml` configuration schema is separate and remains `version: 1`. Do not change it to version 2.
|
|
50
|
+
|
|
51
|
+
## Compatibility
|
|
52
|
+
|
|
53
|
+
JC001–JC007 meanings, severity, JC001/JC003 precedence, static-presence proof, and the base-to-head, head-to-base, and head-to-head directions are unchanged. Exit codes remain 0 for no unsuppressed errors, 1 for compatibility errors, and 2 for tool/config/Git/parser/usage failures. Warnings do not establish compatibility for unknown evidence.
|
|
54
|
+
|
|
55
|
+
## Known limitations
|
|
56
|
+
|
|
57
|
+
ActiveJob, keyword compatibility, payload value types, Hash internal schemas, live Redis inspection, wrappers, aliases, general dataflow, and runtime metaprogramming remain outside scope. An undiscovered producer does not prove that a queue is empty. Analysis does not execute application code.
|
|
58
|
+
|
|
59
|
+
The existing suppression worker-name validator is stricter than producer discovery for some Unicode combining-mark identifiers. Such workers can be analyzed correctly but rejected in `.jobcompat.yml` ignore entries. This pre-existing limitation is a v0.2.1 follow-up candidate; it does not affect general Unicode producer recognition.
|
|
60
|
+
|
|
61
|
+
## Upgrade notes
|
|
62
|
+
|
|
63
|
+
Update JSON consumers to accept schema version 2 and the additional producer uncertainty reasons. Keep configuration version 1. Broader producer discovery may surface compatibility errors or JC007 warnings that v0.1 did not report; review those findings before deployment.
|
|
64
|
+
|
|
65
|
+
Ruby 3.3 or newer and Prism `>= 1.9, < 2` remain required. No Sidekiq runtime dependency is added.
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
After publication, install this version with `gem install jobcompat --version 0.2.0`. Bundler users can specify `gem "jobcompat", "~> 0.2.0", require: false`.
|
|
70
|
+
|
|
71
|
+
Fetch the intended comparison ref, then run `jobcompat check --base origin/main` or `bundle exec jobcompat check --base origin/main` in the application repository.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# jobcompat v0.2.1 release notes
|
|
2
|
+
|
|
3
|
+
jobcompat v0.2.1 is a maintenance release. It makes `.jobcompat.yml` suppressions accept exactly the canonical worker names that producer analysis can report, so a worker that jobcompat can find is also a worker you can suppress. It adds no producer API, compatibility rule, or configuration key.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
|
|
7
|
+
- `ignore[].worker` accepts canonical Unicode worker names, including names with combining marks such as `ÁJob` and qualified names such as `Admin::ÁJob`.
|
|
8
|
+
- Client String worker targets and `ignore[].worker` now use one shared Prism-based canonical worker-name check, so the two can no longer disagree.
|
|
9
|
+
- Worker names keep their exact codepoints. jobcompat performs no Unicode normalization.
|
|
10
|
+
- Malformed, non-constant, and invalid-byte `ignore[].worker` values are reported as configuration errors (exit 2).
|
|
11
|
+
|
|
12
|
+
## Unicode suppression consistency
|
|
13
|
+
|
|
14
|
+
In v0.2.0, producer analysis could report a finding for a worker whose name contains a combining mark, but `.jobcompat.yml` rejected a suppression for that same name with `ignore[0].worker is invalid`. v0.2.1 fixes this. For example, with a worker named `A` + `U+0301 COMBINING ACUTE ACCENT` + `Job` (displayed as `ÁJob`):
|
|
15
|
+
|
|
16
|
+
```yaml
|
|
17
|
+
version: 1
|
|
18
|
+
|
|
19
|
+
ignore:
|
|
20
|
+
- rule: JC003
|
|
21
|
+
worker: ÁJob
|
|
22
|
+
reason: intentional migration
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
suppresses the matching JC003 finding, exits 0, and keeps the usual suppression audit record (rule, exact worker, reason, finding count) in text and JSON output.
|
|
26
|
+
|
|
27
|
+
Matching remains exact. jobcompat applies no NFC, NFD, NFKC, or NFKD normalization and no case folding. `ÁJob` written as the single codepoint `U+00C1` followed by `Job` and `ÁJob` written as `A` + `U+0301` + `Job` look the same but are different worker identities: a suppression for one does not suppress a finding for the other. Write the suppression with the same codepoints as the worker name in your source.
|
|
28
|
+
|
|
29
|
+
## Configuration compatibility note
|
|
30
|
+
|
|
31
|
+
**An existing v0.2.0 configuration can now fail with exit 2.**
|
|
32
|
+
|
|
33
|
+
v0.2.0's configuration validator accepted some `ignore[].worker` strings that Ruby does not treat as constant names. Producer analysis can never report such a name, so those entries could never match a jobcompat finding and were silent no-op suppressions. v0.2.1 rejects them as configuration errors (`ignore[N].worker is invalid`, JSON `status: "failed"`, exit 2) instead of ignoring them.
|
|
34
|
+
|
|
35
|
+
For example, `管理::輸出` is rejected because neither segment starts with an uppercase letter, so Ruby parses it as method calls rather than a constant path. This does not mean CJK worker names are unsupported: `Job管理::E輸出` is a valid Ruby constant path and remains accepted, and analysis and suppression treat it like any other worker name.
|
|
36
|
+
|
|
37
|
+
## Contracts unchanged
|
|
38
|
+
|
|
39
|
+
- JC001–JC007 meanings, severities, and precedence are unchanged.
|
|
40
|
+
- JSON output remains `schema_version: 2`.
|
|
41
|
+
- `.jobcompat.yml` remains `version: 1`.
|
|
42
|
+
- Exit codes are unchanged: 0 for no unsuppressed compatibility errors, 1 for compatibility errors, and 2 for tool, config, Git, parser, or usage failures.
|
|
43
|
+
- Ruby `>= 3.3` and Prism `>= 1.9, < 2` remain required.
|
|
44
|
+
|
|
45
|
+
No JSON or configuration schema migration is required.
|
|
46
|
+
|
|
47
|
+
## Known limitations
|
|
48
|
+
|
|
49
|
+
ActiveJob, keyword compatibility, payload value types, Hash internal schemas, live Redis inspection, wrappers, aliases, general dataflow, and runtime metaprogramming remain outside scope. An undiscovered producer does not prove that a queue is empty. Analysis does not execute application code. See the [normative v0.2.1 delta](spec-v0.2.1.md) for the full canonical worker-name rules.
|
|
50
|
+
|
|
51
|
+
## Upgrade notes
|
|
52
|
+
|
|
53
|
+
If your `ignore[].worker` entries are ordinary Ruby constant names, such as `ExportJob` or `Admin::ExportJob`, no configuration change is needed.
|
|
54
|
+
|
|
55
|
+
If an `ignore[].worker` entry is a string that is not a canonical Ruby constant name, v0.2.1 reports a configuration error and exits 2. That entry never suppressed anything in v0.2.0. Remove it, or correct it to the exact worker name that jobcompat reports in its findings.
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
After publication, install this version with `gem install jobcompat --version 0.2.1`. Bundler users can specify `gem "jobcompat", "~> 0.2.1", require: false`.
|
|
60
|
+
|
|
61
|
+
Fetch the intended comparison ref, then run `jobcompat check --base origin/main` or `bundle exec jobcompat check --base origin/main` in the application repository.
|
data/docs/spec-v0.2.1.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# jobcompat v0.2.1 normative delta specification
|
|
2
|
+
|
|
3
|
+
Status: normative for `0.2.1`
|
|
4
|
+
|
|
5
|
+
This document is a small delta from [`spec-v0.2.md`](spec-v0.2.md), which remains the normative `0.2.0` delta from [`spec-v0.1.md`](spec-v0.1.md). **All v0.1 and v0.2 rules remain normative unless explicitly overridden here.** v0.2.1 is a maintenance release: it adds no producer syntax, rule, finding field, uncertainty reason, or configuration key.
|
|
6
|
+
|
|
7
|
+
## 1. Versioned contracts
|
|
8
|
+
|
|
9
|
+
- Gem and CLI version: `0.2.1`.
|
|
10
|
+
- JSON `schema_version`: `2` (unchanged).
|
|
11
|
+
- `.jobcompat.yml` version: `1` (unchanged; `version: 2` remains a config error).
|
|
12
|
+
|
|
13
|
+
No user migration is required.
|
|
14
|
+
|
|
15
|
+
## 2. Canonical worker-name validation
|
|
16
|
+
|
|
17
|
+
A canonical String worker name is a String that:
|
|
18
|
+
|
|
19
|
+
1. converts safely to valid UTF-8;
|
|
20
|
+
2. parses with the supported Prism (`>= 1.9, < 2`) without diagnostics;
|
|
21
|
+
3. contains exactly one expression;
|
|
22
|
+
4. is a static `ConstantReadNode` or `ConstantPathNode` chain, not root-qualified with a leading `::`;
|
|
23
|
+
5. when flattened and joined with `::`, equals the complete String exactly.
|
|
24
|
+
|
|
25
|
+
Prism is the only grammar authority; jobcompat defines no handwritten identifier, case, combining-mark, or Unicode-range rules. Input is never trimmed. Whitespace, comments, newlines, parentheses, calls, operators, extra statements, `self::`, and any other syntax make the String non-canonical.
|
|
26
|
+
|
|
27
|
+
One internal validator applies this definition to both Client class Strings (`Sidekiq::Client.push` / `push_bulk`) and `.jobcompat.yml` `ignore[].worker`.
|
|
28
|
+
|
|
29
|
+
**Invariant:** any worker name accepted as an exact canonical String worker name by producer analysis MUST also be valid as `.jobcompat.yml` `ignore[].worker`, subject to the same canonical textual identity rules. Conversely, a configured worker name that producer analysis could not accept as a canonical String is invalid configuration.
|
|
30
|
+
|
|
31
|
+
Constant AST resolution (for example `"class" => ExportJob`, `ExportJob.perform_async`, and worker declarations) is unchanged and is not governed by this section.
|
|
32
|
+
|
|
33
|
+
## 3. Suppression semantics
|
|
34
|
+
|
|
35
|
+
Matching remains exact on `rule_id` plus the worker String. There is no Unicode normalization (no NFC, NFD, NFKC, or NFKD), case folding, namespace inference, partial, glob, or regex matching. Configured names are stored and reported exactly as written.
|
|
36
|
+
|
|
37
|
+
Visually similar but codepoint-distinct valid names remain different worker identities. For example, `ÁJob` written as `U+00C1 J o b` and as `U+0041 U+0301 J o b` are distinct: a suppression for one does not suppress a finding for the other, and the two entries are not duplicates of each other. Two entries with the same rule and exactly the same worker String remain a duplicate-entry config error.
|
|
38
|
+
|
|
39
|
+
A qualified name such as `Admin::ÁJob` suppresses only the finding whose worker is exactly that qualified name; `ÁJob` alone does not match it.
|
|
40
|
+
|
|
41
|
+
A matched suppression keeps the existing audit record (`rule_id`, exact `worker`, `reason`, `finding_count`) in JSON and text output, and a suppressed compatibility ERROR no longer contributes to exit 1.
|
|
42
|
+
|
|
43
|
+
## 4. Invalid worker-name behavior
|
|
44
|
+
|
|
45
|
+
The shared validator only decides validity. Each caller owns its failure surface:
|
|
46
|
+
|
|
47
|
+
- **Config:** a non-canonical, non-String, or invalid-encoding `ignore[].worker` is a `config_error` with the existing message `ignore[N].worker is invalid`, JSON `status: "failed"`, and exit 2. A document whose bytes Psych rejects before validation is also a `config_error` with exit 2. No raw `ArgumentError` or `EncodingError`, `internal_error`, or stack trace is produced.
|
|
48
|
+
- **Producer analysis:** a non-canonical or invalid-encoding Client class String remains `unsupported_client_payload` JC007 evidence, as in v0.2.0.
|
|
49
|
+
|
|
50
|
+
Inside the validator only expected encoding failures are treated as unsupported input; unexpected parser or implementation failures stay visible as tool failures.
|
|
51
|
+
|
|
52
|
+
Compatibility note: v0.2.0 also accepted some configured names that Ruby does not parse as constants, such as `管理::輸出` (segments that do not start with an uppercase letter). No finding could ever carry such a worker name, so those entries never suppressed anything; v0.2.1 reports them as `config_error`.
|
|
53
|
+
|
|
54
|
+
## 5. Compatibility invariants
|
|
55
|
+
|
|
56
|
+
- JC001–JC007 semantics, severities, and precedence are unchanged; there is no JC008.
|
|
57
|
+
- Exit codes are unchanged: 0 no unsuppressed compatibility ERROR, 1 compatibility ERROR, 2 tool/config/Git/parser/usage failure.
|
|
58
|
+
- JSON schema 2 structure and `unknown_reason` vocabulary are unchanged.
|
|
59
|
+
- Runtime dependencies remain `prism >= 1.9, < 2`; Ruby `>= 3.3`.
|
|
60
|
+
- Validation parses text only: no eval, `const_get`, constantize, application `require`, Rails or Sidekiq boot, Redis, source execution, or analysis-time network.
|
|
61
|
+
- Output remains byte-deterministic, including exact Unicode codepoints.
|
|
62
|
+
|
|
63
|
+
## 6. Non-goals
|
|
64
|
+
|
|
65
|
+
New Sidekiq producer APIs, SARIF, ActiveJob, keyword compatibility, payload type or Hash schema checking, Redis inspection, wrapper tracking, aliases, inheritance analysis, general dataflow, feature-flag analysis, new suppression syntax, wildcard suppressions, and regex suppressions.
|
data/docs/spec-v0.2.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# jobcompat v0.2 normative delta specification
|
|
2
|
+
|
|
3
|
+
Status: normative for `0.2.0`
|
|
4
|
+
|
|
5
|
+
This document is a delta from [`spec-v0.1.md`](spec-v0.1.md). **v0.1 rules remain normative unless explicitly overridden here.** JC001–JC007, their severity and precedence, the three deployment directions, immutable Git inputs, source-only Prism analysis, and conservative unknown behavior are unchanged.
|
|
6
|
+
|
|
7
|
+
## 1. Versioned contracts
|
|
8
|
+
|
|
9
|
+
- Gem and CLI version: `0.2.0`.
|
|
10
|
+
- JSON `schema_version`: `2`.
|
|
11
|
+
- `.jobcompat.yml` version: `1`.
|
|
12
|
+
- JSON schema v2 preserves the v1 field structure. Its intentional public expansion is the producer `unknown_reason` vocabulary.
|
|
13
|
+
|
|
14
|
+
## 2. Supported producer syntax
|
|
15
|
+
|
|
16
|
+
In addition to all v0.1 forms, v0.2 recognizes:
|
|
17
|
+
|
|
18
|
+
- `Worker.perform_bulk(rows, **options)`;
|
|
19
|
+
- `Worker.set(...).perform_in(schedule, *payload)` and `.perform_at(time, *payload)`;
|
|
20
|
+
- `Worker.set(...).perform_bulk(rows, **options)`;
|
|
21
|
+
- `Sidekiq::Client.push(payload)` and `::Sidekiq::Client.push(payload)`;
|
|
22
|
+
- `Sidekiq::Client.push_bulk(payload)` and its root-qualified form.
|
|
23
|
+
|
|
24
|
+
Only the exact static Client receiver is recognized. Client instances, `via`, wrappers, similarly named receivers, ActiveJob, inline/sync execution, and other bulk-like method names remain unsupported or ignored as defined by the v0.1 evidence policy.
|
|
25
|
+
|
|
26
|
+
## 3. Bulk normalization
|
|
27
|
+
|
|
28
|
+
A static outer Array is inspected without evaluating inner values. Each inner Array with no splat contributes its element count. Duplicate counts are removed and sorted. One callsite therefore emits at most one producer fact per distinct known arity, plus uncertainty evidence. `[]` emits no producer fact and no warning.
|
|
29
|
+
|
|
30
|
+
Non-Array rows and rows containing a splat emit `dynamic_bulk_arguments`; known sibling rows remain known. A dynamic outer collection emits only that unknown evidence. Explicit `at`, `batch_size`, and `spread_interval` options do not contribute to payload arity. A keyword splat that could override bulk class or args emits `dynamic_bulk_options` and does not prove a known payload.
|
|
31
|
+
|
|
32
|
+
The engine receives ordinary single-arity `Call` facts. It has no bulk-specific compatibility rule or arity-set type.
|
|
33
|
+
|
|
34
|
+
## 4. Setter schedules
|
|
35
|
+
|
|
36
|
+
For Setter `perform_in` and `perform_at`, the first outer call argument is the schedule and is excluded from payload arity. Setter options are not payload arguments. Existing Setter `perform_async` behavior is unchanged.
|
|
37
|
+
|
|
38
|
+
## 5. Static Client payloads
|
|
39
|
+
|
|
40
|
+
`push` requires one static Hash payload with runtime-valid string `"class"` and `"args"` keys. `"args"` must be a static Array with no splat; its length is the payload arity. Additional valid string job options do not affect arity.
|
|
41
|
+
|
|
42
|
+
`push_bulk` uses the same class extraction and bulk normalization for `"args"`. The documented symbol forms of `at`, `batch_size`, and `spread_interval` are accepted because Sidekiq consumes them before normalizing job keys; other non-string keys are unsupported.
|
|
43
|
+
|
|
44
|
+
A class constant uses the existing root/qualified/lexical worker resolution. A class String must be a canonical Ruby constant path accepted by the supported Prism parser, without leading `::`, and is matched exactly against the base/head worker union. Surrounding namespaces are never prefixed, and Unicode identifiers are not normalized. Both Client APIs share this validation: safely convert the value to valid UTF-8, parse exactly one static constant expression, reuse constant-path extraction, and require the extracted canonical name to equal the complete String value. Malformed bytes, parser diagnostics, whitespace, comments, extra statements, and other non-canonical syntax produce `unsupported_client_payload` JC007 evidence rather than an internal error.
|
|
45
|
+
|
|
46
|
+
For `"class"` and `"args"` independently, the last Hash entry capable of writing that key determines certainty. A later explicit string key replaces earlier writes; a later unknown Hash splat or dynamic/non-static key makes that key uncertain. Static unrelated keys cannot overwrite it. Parentheses around a single static expression preserve its evidence. Unsupported static non-string keys retain the payload policy above.
|
|
47
|
+
|
|
48
|
+
A known worker target is retained when only args are uncertain, so JC005 and worker-attributed JC007 can coexist. If class is uncertain, an earlier explicit worker is not retained and cannot create a class-presence request or JC005. A dynamic key that prevents proving either relevant value emits `unsupported_client_payload`; a dynamic key before both final explicit keys does not discard their known evidence.
|
|
49
|
+
|
|
50
|
+
## 6. Producer uncertainty reasons
|
|
51
|
+
|
|
52
|
+
Schema v2 adds these deterministic reasons:
|
|
53
|
+
|
|
54
|
+
- `dynamic_bulk_arguments`;
|
|
55
|
+
- `dynamic_bulk_options`;
|
|
56
|
+
- `dynamic_client_payload`;
|
|
57
|
+
- `dynamic_client_class`;
|
|
58
|
+
- `dynamic_client_args`;
|
|
59
|
+
- `unsupported_client_payload`.
|
|
60
|
+
|
|
61
|
+
Recognized but unprovable producer evidence maps to JC007. Known and unknown evidence from one bulk expression may coexist, as may ERROR and WARNING findings. Revision is excluded from the semantic evidence fingerprint exactly as in v0.1.
|
|
62
|
+
|
|
63
|
+
## 7. Determinism and limits
|
|
64
|
+
|
|
65
|
+
Facts sharing one path/line/column are totally ordered by method, arity, and uncertainty reason. Producer arity arrays remain sorted unique integers; finding `payload_arity` remains an integer or null. No source is evaluated, required, or booted; there is no local-variable dataflow, Redis access, Sidekiq runtime dependency, checkout, or working-tree mutation.
|
|
66
|
+
|
|
67
|
+
The Prism constraint remains `>= 1.9, < 2`, and Ruby remains `>= 3.3`.
|
data/lib/jobcompat/analysis.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "prism"
|
|
2
|
+
require_relative "canonical_worker_name"
|
|
2
3
|
|
|
3
4
|
module Jobcompat
|
|
4
5
|
Location = Data.define(:revision, :path, :line, :column, :role, :excerpt) do
|
|
@@ -18,7 +19,7 @@ module Jobcompat
|
|
|
18
19
|
signature_kind: signature_kind, signature: signature, unknown_reason: unknown_reason}
|
|
19
20
|
end
|
|
20
21
|
end
|
|
21
|
-
Call = Data.define(:revision, :path, :location, :receiver, :root, :namespace, :scope, :method, :arity, :unknown_reason, :fingerprint_parts)
|
|
22
|
+
Call = Data.define(:revision, :path, :location, :receiver, :resolution_mode, :root, :namespace, :scope, :method, :arity, :unknown_reason, :fingerprint_parts)
|
|
22
23
|
Unknown = Data.define(:revision, :kind, :reason, :worker, :locations, :fingerprint_parts)
|
|
23
24
|
Fragment = Data.define(:name, :declaration, :includes, :include_tokens, :performs, :lexical_scope)
|
|
24
25
|
|
|
@@ -126,6 +127,7 @@ module Jobcompat
|
|
|
126
127
|
end
|
|
127
128
|
|
|
128
129
|
def self.constant(node)
|
|
130
|
+
node = unwrap_parentheses(node)
|
|
129
131
|
case node
|
|
130
132
|
when Prism::ConstantReadNode then [[node.name.to_s.encode(Encoding::UTF_8)], false]
|
|
131
133
|
when Prism::ConstantPathNode, Prism::ConstantPathTargetNode
|
|
@@ -138,6 +140,13 @@ module Jobcompat
|
|
|
138
140
|
end
|
|
139
141
|
end
|
|
140
142
|
|
|
143
|
+
def self.unwrap_parentheses(node)
|
|
144
|
+
while node.is_a?(Prism::ParenthesesNode) && node.body.is_a?(Prism::StatementsNode) && node.body.body.one?
|
|
145
|
+
node = node.body.body.first
|
|
146
|
+
end
|
|
147
|
+
node
|
|
148
|
+
end
|
|
149
|
+
|
|
141
150
|
def self.normalize(source)
|
|
142
151
|
Prism.lex(source).value.filter_map do |pair|
|
|
143
152
|
token = pair.first
|
|
@@ -237,14 +246,21 @@ module Jobcompat
|
|
|
237
246
|
|
|
238
247
|
def extract_call(node, namespace, scope, statement)
|
|
239
248
|
method = node.name.to_s
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
249
|
+
if %w[push push_bulk].include?(method) && sidekiq_client?(node.receiver)
|
|
250
|
+
extract_client_call(node, namespace, scope, statement, method)
|
|
251
|
+
return
|
|
252
|
+
end
|
|
253
|
+
return unless %w[perform_async perform_in perform_at perform_bulk].include?(method)
|
|
254
|
+
receiver = self.class.unwrap_parentheses(node.receiver)
|
|
255
|
+
set_call = receiver if receiver.is_a?(Prism::CallNode) && receiver.name == :set
|
|
244
256
|
receiver = set_call.receiver if set_call
|
|
245
257
|
constant = self.class.constant(receiver)
|
|
246
258
|
safe = node.call_operator_loc&.slice == "&." || set_call&.call_operator_loc&.slice == "&."
|
|
247
259
|
arguments = node.arguments&.arguments || []
|
|
260
|
+
if method == "perform_bulk"
|
|
261
|
+
extract_worker_bulk(node, namespace, scope, statement, method, receiver, constant, safe, arguments)
|
|
262
|
+
return
|
|
263
|
+
end
|
|
248
264
|
reason = if arguments.any? { |arg| arg.is_a?(Prism::SplatNode) }
|
|
249
265
|
"splat_arguments"
|
|
250
266
|
elsif arguments.any? { |arg| arg.is_a?(Prism::ForwardingArgumentsNode) }
|
|
@@ -259,9 +275,130 @@ module Jobcompat
|
|
|
259
275
|
"safe_navigation_receiver"
|
|
260
276
|
end
|
|
261
277
|
arity = reason && %w[splat_arguments forwarded_arguments missing_schedule_argument].include?(reason) ? nil : arguments.length - (method == "perform_async" ? 0 : 1)
|
|
278
|
+
emit_call(node, namespace, scope, statement, method, constant&.first&.join("::"), "lexical_constant", constant&.last, arity, reason)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def extract_worker_bulk(node, namespace, scope, statement, method, receiver_node, constant, safe, arguments)
|
|
282
|
+
reason = if safe
|
|
283
|
+
"safe_navigation_receiver"
|
|
284
|
+
elsif constant.nil? && !receiver_node.is_a?(Prism::ConstantPathNode)
|
|
285
|
+
"dynamic_receiver"
|
|
286
|
+
elsif constant.nil?
|
|
287
|
+
"unsupported_constant_path"
|
|
288
|
+
elsif arguments.empty?
|
|
289
|
+
"dynamic_bulk_arguments"
|
|
290
|
+
elsif arguments.drop(1).any? { |arg| !arg.is_a?(Prism::KeywordHashNode) || arg.elements.any? { |element| element.is_a?(Prism::AssocSplatNode) } }
|
|
291
|
+
"dynamic_bulk_options"
|
|
292
|
+
end
|
|
293
|
+
receiver = constant&.first&.join("::")
|
|
294
|
+
if reason
|
|
295
|
+
emit_call(node, namespace, scope, statement, method, receiver, "lexical_constant", constant&.last, nil, reason)
|
|
296
|
+
return
|
|
297
|
+
end
|
|
298
|
+
arities, unknown = extract_bulk_arities(arguments.first)
|
|
299
|
+
arities.each do |arity|
|
|
300
|
+
emit_call(node, namespace, scope, statement, method, receiver, "lexical_constant", constant.last, arity, nil)
|
|
301
|
+
end
|
|
302
|
+
if unknown
|
|
303
|
+
emit_call(node, namespace, scope, statement, method, receiver, "lexical_constant", constant.last, nil, "dynamic_bulk_arguments")
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def extract_client_call(node, namespace, scope, statement, method)
|
|
308
|
+
if node.call_operator_loc&.slice == "&."
|
|
309
|
+
emit_call(node, namespace, scope, statement, method, nil, nil, nil, nil, "safe_navigation_receiver")
|
|
310
|
+
return
|
|
311
|
+
end
|
|
312
|
+
arguments = node.arguments&.arguments || []
|
|
313
|
+
payload = arguments.one? ? self.class.unwrap_parentheses(arguments.first) : nil
|
|
314
|
+
unless payload.is_a?(Prism::HashNode) || payload.is_a?(Prism::KeywordHashNode)
|
|
315
|
+
emit_call(node, namespace, scope, statement, method, nil, nil, nil, nil, "dynamic_client_payload")
|
|
316
|
+
return
|
|
317
|
+
end
|
|
318
|
+
associations = payload.elements.select { |element| element.is_a?(Prism::AssocNode) }
|
|
319
|
+
class_node, class_status = client_hash_value(payload.elements, "class")
|
|
320
|
+
args_node, args_status = client_hash_value(payload.elements, "args")
|
|
321
|
+
args_node = self.class.unwrap_parentheses(args_node)
|
|
322
|
+
invalid_keys = associations.any? do |association|
|
|
323
|
+
key = self.class.unwrap_parentheses(association.key)
|
|
324
|
+
next false if key.is_a?(Prism::StringNode)
|
|
325
|
+
next false unless key.static_literal?
|
|
326
|
+
method != "push_bulk" || !key.is_a?(Prism::SymbolNode) || !%w[at batch_size spread_interval].include?(key.unescaped)
|
|
327
|
+
end
|
|
328
|
+
reason = "unsupported_client_payload" if [class_status, args_status].any? { |status| %i[missing unsupported].include?(status) } || invalid_keys
|
|
329
|
+
reason ||= "dynamic_client_class" if class_status == :unknown
|
|
330
|
+
receiver, mode, rooted, class_reason = client_worker(class_node)
|
|
331
|
+
reason ||= class_reason
|
|
332
|
+
reason ||= method == "push" ? "dynamic_client_args" : "dynamic_bulk_arguments" if args_status == :unknown
|
|
333
|
+
if reason
|
|
334
|
+
emit_call(node, namespace, scope, statement, method, receiver, mode, rooted, nil, reason)
|
|
335
|
+
return
|
|
336
|
+
end
|
|
337
|
+
if method == "push"
|
|
338
|
+
unless args_node.is_a?(Prism::ArrayNode) && args_node.elements.none? { |element| element.is_a?(Prism::SplatNode) }
|
|
339
|
+
emit_call(node, namespace, scope, statement, method, receiver, mode, rooted, nil, "dynamic_client_args")
|
|
340
|
+
return
|
|
341
|
+
end
|
|
342
|
+
emit_call(node, namespace, scope, statement, method, receiver, mode, rooted, args_node.elements.length, nil)
|
|
343
|
+
else
|
|
344
|
+
arities, unknown = extract_bulk_arities(args_node)
|
|
345
|
+
arities.each { |arity| emit_call(node, namespace, scope, statement, method, receiver, mode, rooted, arity, nil) }
|
|
346
|
+
if unknown
|
|
347
|
+
emit_call(node, namespace, scope, statement, method, receiver, mode, rooted, nil, "dynamic_bulk_arguments")
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def extract_bulk_arities(node)
|
|
353
|
+
node = self.class.unwrap_parentheses(node)
|
|
354
|
+
return [[], true] unless node.is_a?(Prism::ArrayNode)
|
|
355
|
+
arities = []
|
|
356
|
+
unknown = false
|
|
357
|
+
node.elements.each do |element|
|
|
358
|
+
element = self.class.unwrap_parentheses(element)
|
|
359
|
+
if element.is_a?(Prism::ArrayNode) && element.elements.none? { |item| item.is_a?(Prism::SplatNode) }
|
|
360
|
+
arities << element.elements.length
|
|
361
|
+
else
|
|
362
|
+
unknown = true
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
[arities.uniq.sort, unknown]
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def sidekiq_client?(node)
|
|
369
|
+
constant = self.class.constant(node)
|
|
370
|
+
constant && constant.first == %w[Sidekiq Client]
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def client_hash_value(elements, key)
|
|
374
|
+
elements.reverse_each do |element|
|
|
375
|
+
return [nil, :unknown] if element.is_a?(Prism::AssocSplatNode)
|
|
376
|
+
entry_key = self.class.unwrap_parentheses(element.key)
|
|
377
|
+
if entry_key.is_a?(Prism::StringNode)
|
|
378
|
+
return [element.value, :known] if entry_key.unescaped == key
|
|
379
|
+
elsif !entry_key.static_literal?
|
|
380
|
+
return [nil, :unsupported]
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
[nil, :missing]
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def client_worker(node)
|
|
387
|
+
node = self.class.unwrap_parentheses(node)
|
|
388
|
+
constant = self.class.constant(node)
|
|
389
|
+
return [constant.first.join("::"), "client_constant", constant.last, nil] if constant
|
|
390
|
+
if node.is_a?(Prism::StringNode)
|
|
391
|
+
name = CanonicalWorkerName.parse(node.unescaped)
|
|
392
|
+
return [name, "exact_string", false, nil] if name
|
|
393
|
+
return [nil, nil, nil, "unsupported_client_payload"]
|
|
394
|
+
end
|
|
395
|
+
[nil, nil, nil, "dynamic_client_class"]
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def emit_call(node, namespace, scope, statement, method, receiver, resolution_mode, root, arity, reason)
|
|
262
399
|
expression = self.class.normalize(node.location.slice)
|
|
263
400
|
enclosing = self.class.normalize((statement || node).location.slice)
|
|
264
|
-
@calls << Call.new(@revision, @path, loc(node, reason ? "unknown_call" : "producer"),
|
|
401
|
+
@calls << Call.new(@revision, @path, loc(node, reason ? "unknown_call" : "producer"), receiver, resolution_mode, root,
|
|
265
402
|
namespace, scope, method, arity, reason, [@path, scope, expression, enclosing])
|
|
266
403
|
end
|
|
267
404
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require "prism"
|
|
2
|
+
|
|
3
|
+
module Jobcompat
|
|
4
|
+
# Internal: the single authority for canonical String worker names, shared by
|
|
5
|
+
# producer analysis (Client String "class" values) and Config (ignore[].worker).
|
|
6
|
+
# Prism decides what a Ruby constant path is; no handwritten identifier grammar.
|
|
7
|
+
module CanonicalWorkerName
|
|
8
|
+
# Returns the canonical UTF-8 name when value is exactly one complete,
|
|
9
|
+
# non-root-qualified static constant path, or nil otherwise. The input is
|
|
10
|
+
# never trimmed or Unicode-normalized.
|
|
11
|
+
def self.parse(value)
|
|
12
|
+
return unless value.is_a?(String)
|
|
13
|
+
name = value.encode(Encoding::UTF_8)
|
|
14
|
+
return unless name.valid_encoding?
|
|
15
|
+
result = Prism.parse(name)
|
|
16
|
+
return unless result.errors.empty? && result.value.statements.body.one?
|
|
17
|
+
segments = segments(result.value.statements.body.first)
|
|
18
|
+
return unless segments
|
|
19
|
+
canonical = segments.join("::")
|
|
20
|
+
canonical if canonical == name
|
|
21
|
+
rescue EncodingError
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.segments(node)
|
|
26
|
+
case node
|
|
27
|
+
when Prism::ConstantReadNode then [node.name.to_s.encode(Encoding::UTF_8)]
|
|
28
|
+
when Prism::ConstantPathNode
|
|
29
|
+
parent = node.parent && segments(node.parent)
|
|
30
|
+
parent && parent + [node.name.to_s.encode(Encoding::UTF_8)]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
private_class_method :segments
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/jobcompat/cli.rb
CHANGED
|
@@ -98,7 +98,7 @@ module Jobcompat
|
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
def envelope(status, findings, suppressions, workers, diagnostics, summary)
|
|
101
|
-
{schema_version:
|
|
101
|
+
{schema_version: 2, tool: {name: "jobcompat", version: VERSION}, status: status,
|
|
102
102
|
comparison: {deployment_model: "rolling", base: {ref: @base, sha: @base_sha}, head: {ref: @head, sha: @head_sha}},
|
|
103
103
|
configuration: {path: @loaded_config}, findings: findings, suppressions: suppressions, workers: workers,
|
|
104
104
|
diagnostics: diagnostics, summary: summary}
|
data/lib/jobcompat/config.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "psych"
|
|
2
|
+
require_relative "canonical_worker_name"
|
|
2
3
|
|
|
3
4
|
module Jobcompat
|
|
4
5
|
class Config
|
|
@@ -36,12 +37,7 @@ module Jobcompat
|
|
|
36
37
|
fields!(item, %w[rule worker reason], "ignore[#{index}]")
|
|
37
38
|
raise Error.new("ignore[#{index}] must have rule, worker, reason", category: "config_error") unless item.keys.sort == %w[reason rule worker]
|
|
38
39
|
raise Error.new("ignore[#{index}].rule is invalid", category: "config_error") unless RULES.include?(item["rule"])
|
|
39
|
-
worker
|
|
40
|
-
valid_worker = worker.is_a?(String) && !worker.empty? && !worker.start_with?("::") && worker.split("::", -1).all? do |segment|
|
|
41
|
-
first = segment.each_char.first
|
|
42
|
-
first && (first.match?(/[A-Z]/) || (first.ord > 127 && first.match?(/\p{L}/))) && segment.match?(/\A[\p{Alnum}_]+\z/)
|
|
43
|
-
end
|
|
44
|
-
raise Error.new("ignore[#{index}].worker is invalid", category: "config_error") unless valid_worker
|
|
40
|
+
raise Error.new("ignore[#{index}].worker is invalid", category: "config_error") unless CanonicalWorkerName.parse(item["worker"])
|
|
45
41
|
raise Error.new("ignore[#{index}].reason must be non-blank", category: "config_error") unless item["reason"].is_a?(String) && !item["reason"].strip.empty?
|
|
46
42
|
key = [item["rule"], item["worker"]]
|
|
47
43
|
raise Error.new("duplicate ignore for #{key.join(' ')}", category: "config_error") if seen[key]
|
data/lib/jobcompat/engine.rb
CHANGED
|
@@ -123,10 +123,11 @@ module Jobcompat
|
|
|
123
123
|
private
|
|
124
124
|
|
|
125
125
|
def resolve(raw_calls)
|
|
126
|
-
raw_calls.sort_by { |call| [call.path, call.location.line, call.location.column
|
|
126
|
+
raw_calls.sort_by { |call| [call.path, call.location.line, call.location.column, call.method,
|
|
127
|
+
call.arity.nil? ? Float::INFINITY : call.arity, call.unknown_reason.to_s] }.filter_map do |call|
|
|
127
128
|
worker = nil
|
|
128
129
|
if call.receiver
|
|
129
|
-
candidates = if call.root
|
|
130
|
+
candidates = if call.resolution_mode == "exact_string" || call.root
|
|
130
131
|
[call.receiver]
|
|
131
132
|
elsif call.namespace.nil?
|
|
132
133
|
[]
|
|
@@ -134,9 +135,11 @@ module Jobcompat
|
|
|
134
135
|
(call.namespace.reverse.map { |prefix| "#{prefix}::#{call.receiver}" } + [call.receiver]).uniq
|
|
135
136
|
end
|
|
136
137
|
worker = candidates.find { |name| @names.include?(name) }
|
|
137
|
-
|
|
138
|
+
client_target = %w[client_constant exact_string].include?(call.resolution_mode)
|
|
139
|
+
next unless worker || client_target || (!call.root && call.namespace.nil?)
|
|
138
140
|
end
|
|
139
141
|
reason = call.unknown_reason
|
|
142
|
+
reason ||= "dynamic_client_class" if call.receiver && worker.nil? && %w[client_constant exact_string].include?(call.resolution_mode)
|
|
140
143
|
reason ||= call.namespace.nil? ? "unsupported_constant_path" : "dynamic_receiver" if worker.nil?
|
|
141
144
|
{worker: worker, location: call.location, arity: call.arity, reason: reason, raw: call}
|
|
142
145
|
end
|
data/lib/jobcompat/version.rb
CHANGED
data/lib/jobcompat.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jobcompat
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- jobcompat contributors
|
|
@@ -42,13 +42,22 @@ files:
|
|
|
42
42
|
- SECURITY.md
|
|
43
43
|
- docs/architecture.md
|
|
44
44
|
- docs/competitive-analysis.md
|
|
45
|
+
- docs/implementation-plan-v0.2.1.md
|
|
46
|
+
- docs/implementation-plan-v0.2.md
|
|
45
47
|
- docs/implementation-plan.md
|
|
46
48
|
- docs/release-checklist-v0.1.0.md
|
|
49
|
+
- docs/release-checklist-v0.2.0.md
|
|
50
|
+
- docs/release-checklist-v0.2.1.md
|
|
47
51
|
- docs/release-notes-v0.1.0.md
|
|
52
|
+
- docs/release-notes-v0.2.0.md
|
|
53
|
+
- docs/release-notes-v0.2.1.md
|
|
48
54
|
- docs/spec-v0.1.md
|
|
55
|
+
- docs/spec-v0.2.1.md
|
|
56
|
+
- docs/spec-v0.2.md
|
|
49
57
|
- exe/jobcompat
|
|
50
58
|
- lib/jobcompat.rb
|
|
51
59
|
- lib/jobcompat/analysis.rb
|
|
60
|
+
- lib/jobcompat/canonical_worker_name.rb
|
|
52
61
|
- lib/jobcompat/cli.rb
|
|
53
62
|
- lib/jobcompat/config.rb
|
|
54
63
|
- lib/jobcompat/engine.rb
|