jobcompat 0.2.1 → 0.3.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 +12 -0
- data/README.md +34 -4
- data/SECURITY.md +5 -1
- data/docs/architecture.md +45 -5
- data/docs/implementation-plan-v0.3.1.md +54 -0
- data/docs/implementation-plan-v0.3.md +32 -0
- data/docs/release-checklist-v0.3.0.md +109 -0
- data/docs/release-notes-v0.3.0.md +115 -0
- data/docs/spec-v0.3.1.md +202 -0
- data/docs/spec-v0.3.md +238 -0
- data/lib/jobcompat/aliases.rb +86 -0
- data/lib/jobcompat/analysis.rb +76 -22
- data/lib/jobcompat/cli.rb +12 -3
- data/lib/jobcompat/engine.rb +170 -63
- data/lib/jobcompat/explanation.rb +144 -0
- data/lib/jobcompat/formatter.rb +11 -1
- data/lib/jobcompat/version.rb +1 -1
- data/lib/jobcompat.rb +1 -0
- metadata +9 -1
data/docs/spec-v0.3.1.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# jobcompat v0.3.1 delta specification
|
|
2
|
+
|
|
3
|
+
Status: normative for 0.3.1. This document extends `spec-v0.3.md` (which extends
|
|
4
|
+
`spec-v0.2.1.md`, `spec-v0.2.md`, and `spec-v0.1.md`) with the optional text
|
|
5
|
+
`--explain` output. It adds display capability only. It changes no
|
|
6
|
+
compatibility decision, JSON contract, or exit status policy.
|
|
7
|
+
|
|
8
|
+
## Version and schema contracts
|
|
9
|
+
|
|
10
|
+
Gem and CLI version: 0.3.1. Completed and failed JSON envelopes continue to
|
|
11
|
+
use `schema_version: 3` with the same fields and ordering as 0.3.0.
|
|
12
|
+
`.jobcompat.yml` continues to use config version 1. Ruby >= 3.3 and
|
|
13
|
+
prism >= 1.9, < 2 are unchanged, and no runtime dependency is added.
|
|
14
|
+
|
|
15
|
+
JC001–JC007 semantics, severities, comparison directions, finding precedence,
|
|
16
|
+
finding deduplication, semantic fingerprints, producer identity resolution,
|
|
17
|
+
alias resolution, presence budget, suppression matching, and exit status
|
|
18
|
+
policy are unchanged.
|
|
19
|
+
|
|
20
|
+
## CLI contract
|
|
21
|
+
|
|
22
|
+
`jobcompat check --base REF --explain` is accepted, as are
|
|
23
|
+
`--format text --explain` and `--explain --format text` in either order.
|
|
24
|
+
Option order must not change results.
|
|
25
|
+
|
|
26
|
+
`--format json --explain` (either order) is rejected before any Git or
|
|
27
|
+
parse work: exit 2, category `config_error`, message
|
|
28
|
+
`--explain is supported only with --format text`, and the regular JSON
|
|
29
|
+
failed envelope with `schema_version: 3` on stdout. No stack trace or
|
|
30
|
+
internal exception reaches stderr.
|
|
31
|
+
|
|
32
|
+
`check --help` lists `--explain` with the description
|
|
33
|
+
`Explain the evidence behind each finding (text only)`. Top-level
|
|
34
|
+
`--help` and `--version` output is unchanged except the version string.
|
|
35
|
+
|
|
36
|
+
All existing failure categories (config_error, parse_error, Git errors,
|
|
37
|
+
internal_error) keep their existing streams, exit code 2, diagnostics
|
|
38
|
+
sanitization, and produce no Explain-specific additional diagnostics.
|
|
39
|
+
|
|
40
|
+
## Explain scope
|
|
41
|
+
|
|
42
|
+
`--explain` explains only public findings that remain after targeted
|
|
43
|
+
suppression. Suppressed finding bodies receive no explanation. Internal
|
|
44
|
+
unreported candidates, producers that never became findings, repository
|
|
45
|
+
coverage, runtime queue contents, and hypothetical dynamic producers are out
|
|
46
|
+
of scope. Suppression audit lines, counts, order, and reasons are unchanged.
|
|
47
|
+
|
|
48
|
+
When no findings remain, the existing PASS (or PASS WITH WARNINGS) output is
|
|
49
|
+
emitted with no Explanation heading or section.
|
|
50
|
+
|
|
51
|
+
## Rendering contract
|
|
52
|
+
|
|
53
|
+
For each remaining finding, an Explanation section is inserted between the
|
|
54
|
+
finding's source location list and `Suggested migration:`. All pre-existing
|
|
55
|
+
lines (severity, rule ID, worker, message, Revisions, Reason, Affected
|
|
56
|
+
directions, Risk, locations, remediation, summary, suppression audit) keep
|
|
57
|
+
their content, order, indentation, newlines, and trailing newline. The
|
|
58
|
+
section uses fixed headings:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
Explanation:
|
|
62
|
+
Evidence:
|
|
63
|
+
Decision:
|
|
64
|
+
Boundary:
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`Evidence` contains one or more verified facts used by the decision.
|
|
68
|
+
`Decision` states the rule that produced the finding. `Boundary` states what
|
|
69
|
+
the evidence cannot prove, including conditional assumptions. Each finding
|
|
70
|
+
receives at most one Explanation. Every line passes the existing
|
|
71
|
+
`Formatter.safe_line` sanitization.
|
|
72
|
+
|
|
73
|
+
Without `--explain`, text output is byte-for-byte identical to 0.3.0 apart
|
|
74
|
+
from the version string, and JSON output is byte-for-byte identical to 0.3.0
|
|
75
|
+
apart from `tool.version`.
|
|
76
|
+
|
|
77
|
+
## Per-rule explanation obligations
|
|
78
|
+
|
|
79
|
+
Each Explanation is built from the facts the Engine actually used at the
|
|
80
|
+
finding site.
|
|
81
|
+
|
|
82
|
+
- JC001: BASE producer's decided payload arity, the BASE consumer acceptance,
|
|
83
|
+
the HEAD consumer rejection, and—if `head_to_head` is among the
|
|
84
|
+
directions—a HEAD producer witness of the same arity. No HEAD witness is
|
|
85
|
+
claimed when the direction is absent. Boundary: retained queue contents are
|
|
86
|
+
not inspected.
|
|
87
|
+
- JC002: HEAD producer's decided arity, HEAD consumer acceptance, BASE
|
|
88
|
+
consumer rejection. Boundary: whether an old process consumes the relevant
|
|
89
|
+
queue, and when it retires, is not statically determined.
|
|
90
|
+
- JC003: HEAD producer arity and HEAD consumer rejection only; unrelated
|
|
91
|
+
base-revision evidence must not appear. JC001/JC003 precedence is
|
|
92
|
+
unchanged.
|
|
93
|
+
- JC004: BASE-recognized serialized identity, the conservative HEAD absence
|
|
94
|
+
proof, and provenance. `defined_unrecognized`, `outside_scan_scope`, and
|
|
95
|
+
`unverified` presence statuses are never described as absence. Boundary:
|
|
96
|
+
static absence does not mean the queue is empty.
|
|
97
|
+
- JC005: the decided HEAD serialized identity, HEAD recognition, and the
|
|
98
|
+
BASE absence proof. The rolling-deploy failure is conditional on an old
|
|
99
|
+
process consuming the relevant queue; it is never stated as unconditional.
|
|
100
|
+
A known producer identity suffices when payload arity is unknown. A
|
|
101
|
+
Class-object alias producer is explained under its terminal serialized
|
|
102
|
+
identity, and an exact Client String identity is never rewritten.
|
|
103
|
+
- JC006: BASE contract, HEAD contract, the removed acceptance ranges, and
|
|
104
|
+
the absence of a qualifying producer witness. Boundary: missing producer
|
|
105
|
+
witnesses do not prove absence of queued, scheduled, retried, historical,
|
|
106
|
+
or externally enqueued payloads; no safety or compatibility claim is made.
|
|
107
|
+
- JC007: the existing `unknown_reason`, revision(s), direction(s), and the
|
|
108
|
+
source provenance used for the diagnosis. Unknown or unsupported evidence
|
|
109
|
+
is never presented as proof of compatibility. No new unknown reason is
|
|
110
|
+
introduced. The alias reasons `alias_target_unresolved`, `alias_cycle`,
|
|
111
|
+
`alias_binding_conflict`, and `unsupported_alias_assignment` are reported
|
|
112
|
+
as-is, and an undetermined alias terminal is never presented as resolved.
|
|
113
|
+
|
|
114
|
+
## Alias semantics
|
|
115
|
+
|
|
116
|
+
Serialized identity rules follow 0.3.0. A Class-object producer through an
|
|
117
|
+
alias serializes as the terminal Class name; an exact Client String keeps
|
|
118
|
+
its exact identity. Explanation text never substitutes a source alias token
|
|
119
|
+
for a serialized identity or vice versa.
|
|
120
|
+
|
|
121
|
+
For findings whose evidence involves an alias, Explanation may include one
|
|
122
|
+
line per relevant revision, using only the existing `AliasResolution`
|
|
123
|
+
result:
|
|
124
|
+
|
|
125
|
+
```text
|
|
126
|
+
Alias (HEAD): OldJob -> MiddleJob -> NewJob (resolved).
|
|
127
|
+
Alias (HEAD): unresolved (alias_cycle).
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The Engine never re-walks the alias graph for explanations.
|
|
131
|
+
|
|
132
|
+
## Proof model and isolation
|
|
133
|
+
|
|
134
|
+
Explanations are produced by the Engine at the sites where findings are
|
|
135
|
+
decided, using the producer groups, consumer contracts, presence results,
|
|
136
|
+
alias resolutions, and unknown roots already computed. Re-running
|
|
137
|
+
`Contract#accepts?`, re-walking alias graphs, re-proving absence,
|
|
138
|
+
re-extracting producers, or parsing finding messages to recover evidence is
|
|
139
|
+
prohibited.
|
|
140
|
+
|
|
141
|
+
The Engine exposes `evaluate(with_explanations: false)`. The default return
|
|
142
|
+
value is unchanged. With `with_explanations: true` the result additionally
|
|
143
|
+
carries an internal explanation sidecar keyed by finding object identity, so
|
|
144
|
+
multiple findings of the same rule and worker, JC007 deduplication groups,
|
|
145
|
+
and post-suppression survivors remain correctly paired. Object identity is
|
|
146
|
+
internal only; no identifier is printed or serialized. Findings and worker
|
|
147
|
+
rows never gain an `explanation` key, and the sidecar never reaches JSON.
|
|
148
|
+
|
|
149
|
+
`Formatter.text(envelope, explanations: nil)` renders the section only when
|
|
150
|
+
a non-nil sidecar maps the finding. `explanations: nil` keeps output
|
|
151
|
+
byte-identical. `Formatter.json` is unchanged.
|
|
152
|
+
Suppression continues to run after evaluation and before formatting;
|
|
153
|
+
explanations are displayed only for surviving findings.
|
|
154
|
+
|
|
155
|
+
Call-site counts in Evidence count distinct enqueue locations, keyed by
|
|
156
|
+
revision, path, line, and column. Location role is not part of that identity,
|
|
157
|
+
worker declarations and alias assignments are never counted, and multiple
|
|
158
|
+
arities or known/unknown records originating from one enqueue location count
|
|
159
|
+
once, so one bulk enqueue with several rows is one call site.
|
|
160
|
+
|
|
161
|
+
## Determinism and security
|
|
162
|
+
|
|
163
|
+
For the same repository state, configuration, and command, repeated runs
|
|
164
|
+
produce byte-identical stdout, byte-identical stderr, and identical exit
|
|
165
|
+
codes, with and without `--explain`. Explanation construction avoids
|
|
166
|
+
insertion-order dependence, uses the existing stable location and chain
|
|
167
|
+
ordering, emits no timestamps, randomness, object IDs, or absolute temporary
|
|
168
|
+
paths, performs no Unicode normalization, and cannot inject raw newlines,
|
|
169
|
+
ANSI escapes, or Unicode format controls (existing `safe_line` escaping
|
|
170
|
+
applies). Error-path secret sanitization and stdout/stderr responsibilities
|
|
171
|
+
are unchanged.
|
|
172
|
+
|
|
173
|
+
## Test acceptance criteria
|
|
174
|
+
|
|
175
|
+
- CLI: `--explain` accepted alone and with `--format text` in both orders;
|
|
176
|
+
`--format json --explain` rejected with exit 2, `config_error`, schema 3
|
|
177
|
+
failed envelope; help lists `--explain`; help/version unchanged; invalid
|
|
178
|
+
config, invalid ref, and parse errors keep existing failure contracts.
|
|
179
|
+
- JC001–JC007: per-rule evidence content, precedence, absence conservatism,
|
|
180
|
+
conditional rolling-deploy wording for JC005, no empty-queue claims for
|
|
181
|
+
JC006, unknown-not-compatible for JC007.
|
|
182
|
+
- Alias: resolved BASE/HEAD chains, finite chains, unsupported and unknown
|
|
183
|
+
aliases, direct workers, and Class-object versus exact String contrast.
|
|
184
|
+
- Suppression: suppressed findings receive no Explanation; surviving
|
|
185
|
+
findings do; audit unchanged.
|
|
186
|
+
- Backward compatibility: default text byte-identical to 0.3.0 fixtures
|
|
187
|
+
except the version string; JSON identical except `tool.version`;
|
|
188
|
+
normalized comparison must not mask findings, summary, provenance, or
|
|
189
|
+
unknown reasons.
|
|
190
|
+
- Determinism/security: double-run stdout/stderr/exit equality, control
|
|
191
|
+
characters, ANSI escapes, Unicode format controls, Unicode worker names,
|
|
192
|
+
newlines in refs, dirty worktree neutrality, no source execution, no
|
|
193
|
+
network.
|
|
194
|
+
- No findings: exit 0 with no added Explanation output; warning-only exit 0;
|
|
195
|
+
any errors exit 1.
|
|
196
|
+
|
|
197
|
+
## Non-goals
|
|
198
|
+
|
|
199
|
+
No new compatibility rule, severity, direction, unknown reason, JSON field,
|
|
200
|
+
config key, dependency, queue/runtime simulation, heuristic rename
|
|
201
|
+
detection, or change to existing normative specifications. Explanations do
|
|
202
|
+
not restate suppressed findings and do not estimate runtime behavior.
|
data/docs/spec-v0.3.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# jobcompat v0.3.0 delta specification
|
|
2
|
+
|
|
3
|
+
Status: normative for 0.3.0. This document extends `spec-v0.2.1.md`,
|
|
4
|
+
which retains the v0.2 and v0.1 contracts except where overridden here.
|
|
5
|
+
MUST and MUST NOT state requirements. No application Ruby is executed.
|
|
6
|
+
|
|
7
|
+
## Versioned and frozen contracts
|
|
8
|
+
|
|
9
|
+
Gem and CLI version: 0.3.0. Completed and failed JSON envelopes MUST use
|
|
10
|
+
`schema_version: 3`. Configuration version remains exactly 1; version 2 is
|
|
11
|
+
invalid. Ruby >= 3.3 and Prism >= 1.9, < 2 remain unchanged. No dependency
|
|
12
|
+
on Sidekiq is introduced. JC001-JC007 IDs, severities (JC001-JC005 ERROR,
|
|
13
|
+
JC006-JC007 WARNING), exit policy, suppression identity, and deployment
|
|
14
|
+
directions remain unchanged. ERROR requires provable structural
|
|
15
|
+
incompatibility. Unknown and unsupported evidence never establishes
|
|
16
|
+
compatibility; missing producers never establishes an empty queue.
|
|
17
|
+
Analysis reads committed Git objects and ignores working-tree edits.
|
|
18
|
+
|
|
19
|
+
## Terminology and serialization
|
|
20
|
+
|
|
21
|
+
**Serialized worker identity** is the exact String in a Sidekiq payload's
|
|
22
|
+
`"class"` field. All compatibility comparisons use that identity.
|
|
23
|
+
**Direct worker** is a canonical class recognized through the existing
|
|
24
|
+
direct `Sidekiq::Job` or legacy `Sidekiq::Worker` include and reopened-class
|
|
25
|
+
discovery. **Compatibility alias** is a statically provable constant
|
|
26
|
+
binding to another constant whose graph terminates in one recognized
|
|
27
|
+
direct worker in the same revision. **Alias terminal** is that final
|
|
28
|
+
direct worker after zero or more supported edges.
|
|
29
|
+
**Class-object producer** passes a Ruby Class object (worker APIs,
|
|
30
|
+
Setter APIs, or Client constant payloads). **Exact-string producer** passes
|
|
31
|
+
an already serialized identity through Client push or push_bulk.
|
|
32
|
+
|
|
33
|
+
For `OldJob = NewJob`, Ruby retains the Class object's canonical name
|
|
34
|
+
`NewJob`. Class-object producers through OldJob MUST therefore normalize
|
|
35
|
+
to NewJob. This applies to perform_async, perform_in, perform_at,
|
|
36
|
+
perform_bulk, all four `.set(...)` forms, and Client push/push_bulk with a
|
|
37
|
+
constant. A Client `"class" => "OldJob"` MUST retain exactly OldJob on the
|
|
38
|
+
producer side; consumer lookup MAY resolve OldJob through its alias.
|
|
39
|
+
There is no namespace prefixing or Unicode normalization of exact Strings.
|
|
40
|
+
|
|
41
|
+
The model freezes Sidekiq's Class-to-String normalization as observed in
|
|
42
|
+
[Sidekiq 8.1.6 JobUtil](https://github.com/sidekiq/sidekiq/blob/v8.1.6/lib/sidekiq/job_util.rb#L38-L55)
|
|
43
|
+
and its [Class-object producer path](https://github.com/sidekiq/sidekiq/blob/v8.1.6/lib/sidekiq/job.rb#L191-L196).
|
|
44
|
+
The [official rename FAQ](https://github.com/sidekiq/sidekiq/wiki/FAQ#how-do-i-safely-rename-a-worker)
|
|
45
|
+
describes a compatibility alias for retained old payloads. Client Strings
|
|
46
|
+
have different job-option/default semantics; their identity behavior is
|
|
47
|
+
documented here without recommending them as a migration strategy.
|
|
48
|
+
|
|
49
|
+
## Supported syntax and namespace semantics
|
|
50
|
+
|
|
51
|
+
Only Prism ConstantWriteNode and ConstantPathWriteNode with plain `=`
|
|
52
|
+
and a ConstantReadNode or static ConstantPathNode RHS after safe
|
|
53
|
+
single-expression parentheses unwrapping prove an alias. The assignment
|
|
54
|
+
MUST be a direct unconditional statement in the Program body or a valid
|
|
55
|
+
Class/Module body, with all enclosing contexts supported.
|
|
56
|
+
|
|
57
|
+
Supported examples are `OldJob = NewJob`, `OldJob = (NewJob)`,
|
|
58
|
+
`::OldJob = ::NewJob`, top-level `Admin::OldJob = Admin::NewJob`, and
|
|
59
|
+
`module Admin; OldJob = NewJob; end`. Existing declaration rules determine
|
|
60
|
+
the LHS canonical name. An unrooted qualified LHS in lexical nesting or
|
|
61
|
+
an unsupported namespace MUST NOT prove an alias. An unrooted qualified
|
|
62
|
+
RHS inside lexical nesting is unsupported_alias_assignment. Rooted RHS
|
|
63
|
+
references and qualified top-level RHS references are exact.
|
|
64
|
+
|
|
65
|
+
Unqualified RHS lookup uses the existing lexical namespace stack in
|
|
66
|
+
reverse order, then the root name. For nested Admin/Jobs, NewJob candidates
|
|
67
|
+
are Admin::Jobs::NewJob, Admin::NewJob, NewJob. The first selected-source
|
|
68
|
+
binding wins lookup; a binding with an unknown value MUST block fallback
|
|
69
|
+
to a farther known worker. No inherited-constant lookup is added.
|
|
70
|
+
|
|
71
|
+
Assignments below if/unless/case/loops/blocks/lambdas/begin/rescue/ensure,
|
|
72
|
+
modifier conditions, singleton classes, methods, or expression wrappers
|
|
73
|
+
are unsupported. Even an unconditional begin wrapper is unsupported.
|
|
74
|
+
Or/and/operator writes, multiple assignment targets, dynamic RHS,
|
|
75
|
+
ternary RHS, calls, and Class.new MUST NOT become resolved aliases.
|
|
76
|
+
Named unsupported writes enter the inventory so relevant identities can
|
|
77
|
+
warn and conflicts cannot be hidden. const_set/autoload/const_missing
|
|
78
|
+
remain outside alias discovery.
|
|
79
|
+
|
|
80
|
+
## Selected binding inventory and graph
|
|
81
|
+
|
|
82
|
+
During the existing Prism traversal, record selected-source class/module
|
|
83
|
+
declarations and named writes, plus alias candidates. Presence facts in
|
|
84
|
+
DefinedConstantIndex MUST remain separate from alias graph facts.
|
|
85
|
+
Exactly one supported alias binding and no other binding to its LHS is
|
|
86
|
+
required. Duplicate aliases, even identical ones, alias plus class/module,
|
|
87
|
+
or alias plus unsupported writes produce alias_binding_conflict. No
|
|
88
|
+
source-order winner is selected. Existing reopened direct class fragments
|
|
89
|
+
are allowed and merged as before; they are not conflicts by themselves.
|
|
90
|
+
An alias that reaches a conflicting binding MUST remain unknown.
|
|
91
|
+
Selected ambiguous constant paths also retain leaf-name conflict evidence:
|
|
92
|
+
an assignment or declaration such as `self::OldJob = OtherJob` cannot be
|
|
93
|
+
ignored when its leaf may affect an alias or a reached terminal. These
|
|
94
|
+
facts conservatively block resolution without evaluating the receiver.
|
|
95
|
+
Excluded or differently named ambiguous facts do not enter this proof.
|
|
96
|
+
|
|
97
|
+
Each revision resolves its own graph independently. Finite chains have
|
|
98
|
+
no arbitrary length cap. Iterative traversal detects cycles without
|
|
99
|
+
recursive stack overflow. Resolved chain order follows edges, for example
|
|
100
|
+
`[OldJob, MiddleJob, NewJob]`. A cycle chain closes with its repeated
|
|
101
|
+
identity, for example `[OldJob, MiddleJob, OldJob]`. A missing or
|
|
102
|
+
unrecognized terminal is alias_target_unresolved. Failed suffixes
|
|
103
|
+
propagate their reason and proof to aliases depending on them.
|
|
104
|
+
|
|
105
|
+
Only selected files supply trusted edges. Excluded declarations/writes
|
|
106
|
+
may block absence through the presence pass, but MUST NOT resolve aliases.
|
|
107
|
+
An excluded OldJob alias yields outside_analysis_scope for a removal
|
|
108
|
+
transition; a selected alias targeting an excluded worker yields
|
|
109
|
+
alias_target_unresolved. File movement alone does not alter identity
|
|
110
|
+
compatibility. All keys preserve Prism's exact UTF-8 codepoints.
|
|
111
|
+
|
|
112
|
+
## Effective consumers and relevant identities
|
|
113
|
+
|
|
114
|
+
Snapshot workers remain directly recognized workers. An effective
|
|
115
|
+
consumer map resolves serialized identity S to either direct worker S or
|
|
116
|
+
the terminal contract of supported alias S. A conflicting alias masks an
|
|
117
|
+
otherwise recognized direct consumer at the same identity. Resolved
|
|
118
|
+
aliases retain resolution status even when the terminal signature is
|
|
119
|
+
unknown (keyword_parameters, missing_perform, multiple_perform_definitions).
|
|
120
|
+
Effective alias contracts carry every edge location and the terminal's
|
|
121
|
+
declaration/include/perform proof. No new location role is introduced.
|
|
122
|
+
|
|
123
|
+
Comparison identities MUST include all direct workers in either revision,
|
|
124
|
+
all resolved BASE aliases (historical queues may retain their names),
|
|
125
|
+
aliases present in both revisions, aliases matching an opposite direct
|
|
126
|
+
worker, and aliases referenced by visible producers or needed to explain
|
|
127
|
+
unknown producer identity. A dormant HEAD-only alias is not automatically
|
|
128
|
+
a worker result or standalone warning. The full graph may still be built.
|
|
129
|
+
The common-alias criterion refers to constant-to-constant candidates,
|
|
130
|
+
including unsupported static forms; unrelated dynamic/literal writes such
|
|
131
|
+
as VERSION = "1" do not become relevant merely by existing in both
|
|
132
|
+
revisions. They still block conflicting bindings and are diagnosed when
|
|
133
|
+
referenced by a producer or matched to an opposite worker identity.
|
|
134
|
+
|
|
135
|
+
## Producer resolution
|
|
136
|
+
|
|
137
|
+
Class-object resolution first performs existing lexical/root lookup in
|
|
138
|
+
the producer's own snapshot, including alias bindings. A resolved alias
|
|
139
|
+
normalizes to its terminal direct identity. An unknown alias MUST NOT
|
|
140
|
+
guess its syntactic name as a serialized identity. Such a call retains a
|
|
141
|
+
separate attributed alias name for diagnostics and comparison relevance,
|
|
142
|
+
but cannot drive JC005. Existing cross-revision non-alias attribution
|
|
143
|
+
remains available only when no local alias/binding narrows that proof.
|
|
144
|
+
When alias lookup applies, a nearer selected non-worker class/module
|
|
145
|
+
binding stops fallback and leaves producer identity unknown. Opposite
|
|
146
|
+
revision names cannot override a selected local binding. Normalized
|
|
147
|
+
Class-object alias producers retain edge and terminal declaration,
|
|
148
|
+
include, and perform locations; unresolved targets retain any selected
|
|
149
|
+
declaration that prevents recognition.
|
|
150
|
+
|
|
151
|
+
Exact-string resolution remains exact and does not canonicalize aliases.
|
|
152
|
+
Producer identity certainty and arity certainty MUST be distinct:
|
|
153
|
+
resolved alias plus `*args` retains the terminal identity and splat_arguments;
|
|
154
|
+
unknown alias plus literal args retains unknown identity. Known identity
|
|
155
|
+
with unknown arity can still drive JC005. Existing Hash source-order,
|
|
156
|
+
dynamic-key, bulk-row, lexical producer, and Client String rules persist.
|
|
157
|
+
|
|
158
|
+
## Rules, unknown reasons, and proof
|
|
159
|
+
|
|
160
|
+
JC001: old serialized payload accepted by BASE but rejected by HEAD,
|
|
161
|
+
including alias consumers. JC002: HEAD accepted payload rejected by BASE,
|
|
162
|
+
under its serialized identity. JC003: HEAD producer/consumer mismatch.
|
|
163
|
+
JC001 owns a repeated HEAD mismatch at the same identity/arity before
|
|
164
|
+
JC003. JC001 OldJob and JC003 NewJob from a Class alias producer remain
|
|
165
|
+
distinct findings. JC006 narrowing/witness precedence remains unchanged.
|
|
166
|
+
|
|
167
|
+
JC004 requires absence of a BASE effective serialized identity in HEAD.
|
|
168
|
+
A resolved alias blocks removal; a relevant unknown alias warns rather
|
|
169
|
+
than passing or proving absence. Removing a BASE resolved alias can cause
|
|
170
|
+
JC004 even if its terminal still exists. JC005 requires HEAD known
|
|
171
|
+
serialized producer identity with an effective HEAD consumer and proven
|
|
172
|
+
BASE absence, conditional on old processes consuming that queue. An
|
|
173
|
+
exact-string alias producer can cause JC005 for the alias; a Class-object
|
|
174
|
+
alias producer can cause JC005 only for the terminal. BASE resolved aliases
|
|
175
|
+
block absence; unresolved aliases warn. Existing presence budget/error
|
|
176
|
+
behavior and conservative absence proof remain frozen.
|
|
177
|
+
|
|
178
|
+
JC007 adds exactly four reasons: alias_target_unresolved (supported edge
|
|
179
|
+
does not reach a recognized worker), alias_cycle, alias_binding_conflict,
|
|
180
|
+
and unsupported_alias_assignment. Existing contract/arity reasons retain
|
|
181
|
+
their meanings. Alias warning messages describe the unresolved binding
|
|
182
|
+
and risk to the persisted identity; remediation suggests an explicit
|
|
183
|
+
static alias only if it matches runtime behavior, or manual inspection.
|
|
184
|
+
|
|
185
|
+
BASE alias uncertainty affects head_to_base. HEAD alias uncertainty affects
|
|
186
|
+
base_to_head and additionally head_to_head only with a HEAD producer for
|
|
187
|
+
that same serialized identity. A Class call normalizing to another name
|
|
188
|
+
does not manufacture head_to_head for its source alias. Alias evidence
|
|
189
|
+
fingerprints contain reason, exact identity, lexical scope, normalized
|
|
190
|
+
assignment/enclosing tokens, target reference, and structural chain, but
|
|
191
|
+
exclude revision, paths, lines, and columns. Stable binding/occurrence
|
|
192
|
+
ordering preserves distinct evidence and cross-revision aggregation.
|
|
193
|
+
|
|
194
|
+
## JSON schema 3 and determinism
|
|
195
|
+
|
|
196
|
+
workers[] now represents compared serialized identities, sorted by exact
|
|
197
|
+
name. Every result has base_alias and head_alias, null for a direct or
|
|
198
|
+
absent binding, otherwise an object with exactly status, target, chain,
|
|
199
|
+
unknown_reason. Resolved status has terminal target and null reason;
|
|
200
|
+
unknown status has null target and its alias reason. Conflict or unsupported
|
|
201
|
+
assignment chains start with the alias alone; unresolved target chains
|
|
202
|
+
include the missing target, and cycle chains close the cycle.
|
|
203
|
+
|
|
204
|
+
Presence values are recognized_worker, resolved_alias,
|
|
205
|
+
defined_unrecognized, outside_scan_scope, absent, unverified, not_checked.
|
|
206
|
+
Unknown aliases use defined_unrecognized even in a direct binding conflict.
|
|
207
|
+
Effective contracts appear in base_contract/head_contract for resolved
|
|
208
|
+
aliases; unresolved aliases have null contracts. Producer arities and
|
|
209
|
+
matrix statuses use known serialized identity and effective consumers.
|
|
210
|
+
summary.workers continues to count direct worker facts only, never aliases.
|
|
211
|
+
Suppressions match the finding's exact serialized worker identity.
|
|
212
|
+
|
|
213
|
+
Inputs and graph processing use stable sorting. Chains follow structural
|
|
214
|
+
edge order. Repeated text/JSON for identical refs/config/toolchain MUST be
|
|
215
|
+
byte-identical, including cycles, conflicts, and Unicode names.
|
|
216
|
+
|
|
217
|
+
## Boundaries and acceptance
|
|
218
|
+
|
|
219
|
+
Recognition assumes normal successful application boot, as direct worker
|
|
220
|
+
recognition already does; it does not prove file load order or Zeitwerk
|
|
221
|
+
behavior. No general constant evaluator/dataflow, inheritance/concerns,
|
|
222
|
+
source execution, Rails/Sidekiq boot, Redis/network access, keyword
|
|
223
|
+
compatibility, value/Hash schema analysis, ActiveJob, Client instances,
|
|
224
|
+
wrappers, queue/feature-flag simulation, manual alias config, heuristic
|
|
225
|
+
rename detection, JC008, SARIF, --explain, or coverage report is added.
|
|
226
|
+
|
|
227
|
+
Acceptance requires supported AST/context and namespace tests, finite and
|
|
228
|
+
cross-file graph tests, cycles/conflicts/unknown terminals, reopened and
|
|
229
|
+
unknown-contract targets, all Class-object and exact-string producer
|
|
230
|
+
families, separate identity/arity uncertainty, every JC rule and precedence,
|
|
231
|
+
staged rename and alias removal, excluded-source presence proof, exact
|
|
232
|
+
Unicode/suppression behavior, schema 3 (success and failure), unchanged
|
|
233
|
+
config version 1, deterministic output, existing regressions, source
|
|
234
|
+
non-execution/worktree/index immutability, offline tests, gem packaging,
|
|
235
|
+
isolated CLI install/scenarios, and full validation in UTF-8 locale.
|
|
236
|
+
Locally unavailable Ruby 3.3/4.0 matrix entries remain official CI work.
|
|
237
|
+
The implementation phase MUST leave an uncommitted candidate and MUST NOT
|
|
238
|
+
commit, push, tag, release, publish, or alter historical v0.2.1 artifacts.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module Jobcompat
|
|
2
|
+
ConstantBinding = Data.define(:name, :kind, :location, :fingerprint_parts)
|
|
3
|
+
AliasCandidate = Data.define(:name, :reference, :root, :namespace, :supported, :location, :fingerprint_parts)
|
|
4
|
+
AliasResolution = Data.define(:name, :status, :target, :chain, :unknown_reason, :locations, :fingerprint_parts, :static_reference) do
|
|
5
|
+
def resolved? = status == "resolved"
|
|
6
|
+
def as_json
|
|
7
|
+
{status: status, target: target, chain: chain, unknown_reason: unknown_reason}
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module ConstantLookup
|
|
12
|
+
def self.candidates(reference, root, namespace)
|
|
13
|
+
return [reference] if root
|
|
14
|
+
return [] if namespace.nil?
|
|
15
|
+
(namespace.reverse.map { |prefix| "#{prefix}::#{reference}" } + [reference]).uniq
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class AliasResolver
|
|
20
|
+
def initialize(workers, bindings, candidates, ambiguous_bindings = [])
|
|
21
|
+
@workers = workers
|
|
22
|
+
@bindings = bindings.group_by(&:name)
|
|
23
|
+
@candidates = candidates.group_by(&:name)
|
|
24
|
+
@ambiguous_bindings = ambiguous_bindings.group_by(&:name)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def resolve
|
|
28
|
+
@candidates.keys.sort.to_h { |name| [name, resolve_identity(name)] }.freeze
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def resolve_identity(name)
|
|
34
|
+
chain, locations, parts, visited = [], [], [], {}
|
|
35
|
+
current = name
|
|
36
|
+
reason = nil
|
|
37
|
+
loop do
|
|
38
|
+
chain << current
|
|
39
|
+
if visited[current]
|
|
40
|
+
reason = "alias_cycle"
|
|
41
|
+
break
|
|
42
|
+
end
|
|
43
|
+
visited[current] = true
|
|
44
|
+
candidates = @candidates.fetch(current, [])
|
|
45
|
+
bindings = @bindings.fetch(current, []).sort_by { |item| [item.location.path, item.location.line, item.location.column] }
|
|
46
|
+
ambiguous = @ambiguous_bindings.fetch(current.split("::").last, []).reject { |item| bindings.any? { |binding| binding.location == item.location } }
|
|
47
|
+
if candidates.empty? && @workers.key?(current) && ambiguous.empty?
|
|
48
|
+
return resolution(name, current, chain, nil, locations, parts)
|
|
49
|
+
end
|
|
50
|
+
locations.concat((bindings + ambiguous).map(&:location))
|
|
51
|
+
parts << [current, (bindings + ambiguous).map(&:fingerprint_parts).sort_by(&:inspect)]
|
|
52
|
+
unless ambiguous.empty?
|
|
53
|
+
reason = "alias_binding_conflict"
|
|
54
|
+
break
|
|
55
|
+
end
|
|
56
|
+
if candidates.empty?
|
|
57
|
+
reason = "alias_target_unresolved"
|
|
58
|
+
break
|
|
59
|
+
end
|
|
60
|
+
if bindings.length != 1
|
|
61
|
+
reason = "alias_binding_conflict"
|
|
62
|
+
break
|
|
63
|
+
end
|
|
64
|
+
candidate = candidates.first
|
|
65
|
+
unless candidate.supported
|
|
66
|
+
reason = "unsupported_alias_assignment"
|
|
67
|
+
break
|
|
68
|
+
end
|
|
69
|
+
lookup = ConstantLookup.candidates(candidate.reference, candidate.root, candidate.namespace)
|
|
70
|
+
target = lookup.find { |identity| @bindings.key?(identity) || @workers.key?(identity) }
|
|
71
|
+
unless target
|
|
72
|
+
chain << lookup.first
|
|
73
|
+
reason = "alias_target_unresolved"
|
|
74
|
+
break
|
|
75
|
+
end
|
|
76
|
+
current = target
|
|
77
|
+
end
|
|
78
|
+
resolution(name, nil, chain, reason, locations, parts)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def resolution(name, target, chain, reason, locations, parts)
|
|
82
|
+
AliasResolution.new(name, reason ? "unknown" : "resolved", target, chain.freeze, reason,
|
|
83
|
+
locations.uniq.freeze, parts.freeze, @candidates.fetch(name).any? { |candidate| candidate.reference })
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|