jobcompat 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89c7b2c39f3fbd8e9e683deecc9a5edf2021f6b3ba9b60350635519621b28048
4
- data.tar.gz: c6beb6202b88b1bfcc4536d83479c82005e45d920d555ec5f3645e626e5d2aa2
3
+ metadata.gz: 47c2a0eafb9ae4ced87a7e13389e1ebd043a281d5b711f79bf668179592fea33
4
+ data.tar.gz: a2447a5027360b6bf7843c6c7766b6b8af1977a1e8308700d298e956b428d87e
5
5
  SHA512:
6
- metadata.gz: c730b84dc08928632344fa3e539db97c7a723356b19ba3e89a2c3f9efc0c40979c4e9ab26bf9ff6ad92c1c8beefda83e519c57eca7cde7230c169d8f5e31df4c
7
- data.tar.gz: 0d79be4a2b98032937d8f7c926722ac6a57ff4f899d408a17ecfdd64bb702e823e624d367a2921b3b251b21dff80cb7bf1394f45c322f3f588f5987c6c86824b
6
+ metadata.gz: f6d44307647aed53748ca8b429ecf7469bb79fac73644a9f559a4b7c89c97bb6c52d60da03edd604b787e4959366e775579fa7a140341f1153902eefda90f528
7
+ data.tar.gz: e338de388c16b907edcd05377460bbb741bc268033821a952e0e1ce65e49cc46bbe45348ca30414e65faddb3d9c7d179e15c5538c92373a07ae76e294c787f31
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
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
+
3
11
  ## 0.2.0 - 2026-10-02
4
12
 
5
13
  Broader native Sidekiq producer coverage:
data/README.md CHANGED
@@ -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
 
@@ -171,7 +171,7 @@ Possible future work includes SARIF output and additional evidence sources. Othe
171
171
 
172
172
  ## Contributing
173
173
 
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.
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.
175
175
 
176
176
  ## License
177
177
 
data/docs/architecture.md CHANGED
@@ -191,7 +191,7 @@ It receives source bytes and a revision/path context. It does not open files, in
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 one pure analysis helper shared by `push` and `push_bulk`. After safe UTF-8 conversion and encoding validation, Prism parses the value in isolation. Only a single `ConstantReadNode` or static `ConstantPathNode` is accepted, using the existing constant extractor. 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.
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
195
 
196
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.
197
197
 
@@ -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,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,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.
@@ -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.
@@ -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
@@ -387,28 +388,13 @@ module Jobcompat
387
388
  constant = self.class.constant(node)
388
389
  return [constant.first.join("::"), "client_constant", constant.last, nil] if constant
389
390
  if node.is_a?(Prism::StringNode)
390
- name = canonical_client_string_worker_name(node.unescaped)
391
+ name = CanonicalWorkerName.parse(node.unescaped)
391
392
  return [name, "exact_string", false, nil] if name
392
393
  return [nil, nil, nil, "unsupported_client_payload"]
393
394
  end
394
395
  [nil, nil, nil, "dynamic_client_class"]
395
396
  end
396
397
 
397
- def canonical_client_string_worker_name(value)
398
- name = value.encode(Encoding::UTF_8)
399
- return unless name.valid_encoding?
400
- result = Prism.parse(name)
401
- return unless result.errors.empty? && result.value.statements.body.one?
402
- node = result.value.statements.body.first
403
- return unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
404
- constant = self.class.constant(node)
405
- return unless constant && !constant.last
406
- canonical_name = constant.first.join("::")
407
- canonical_name if canonical_name == name
408
- rescue EncodingError
409
- nil
410
- end
411
-
412
398
  def emit_call(node, namespace, scope, statement, method, receiver, resolution_mode, root, arity, reason)
413
399
  expression = self.class.normalize(node.location.slice)
414
400
  enclosing = self.class.normalize((statement || node).location.slice)
@@ -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
@@ -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 = item["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]
@@ -1,3 +1,3 @@
1
1
  module Jobcompat
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.1"
3
3
  end
data/lib/jobcompat.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require_relative "jobcompat/version"
2
2
  require_relative "jobcompat/errors"
3
+ require_relative "jobcompat/canonical_worker_name"
3
4
  require_relative "jobcompat/config"
4
5
  require_relative "jobcompat/git_repository"
5
6
  require_relative "jobcompat/analysis"
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.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - jobcompat contributors
@@ -42,17 +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
45
46
  - docs/implementation-plan-v0.2.md
46
47
  - docs/implementation-plan.md
47
48
  - docs/release-checklist-v0.1.0.md
48
49
  - docs/release-checklist-v0.2.0.md
50
+ - docs/release-checklist-v0.2.1.md
49
51
  - docs/release-notes-v0.1.0.md
50
52
  - docs/release-notes-v0.2.0.md
53
+ - docs/release-notes-v0.2.1.md
51
54
  - docs/spec-v0.1.md
55
+ - docs/spec-v0.2.1.md
52
56
  - docs/spec-v0.2.md
53
57
  - exe/jobcompat
54
58
  - lib/jobcompat.rb
55
59
  - lib/jobcompat/analysis.rb
60
+ - lib/jobcompat/canonical_worker_name.rb
56
61
  - lib/jobcompat/cli.rb
57
62
  - lib/jobcompat/config.rb
58
63
  - lib/jobcompat/engine.rb