Skip to content

feat(oferta): pay-transparency disclosure signals — jurisdiction table + range-width cap (strong) + missing-range (corroborating-only) - #2021

Merged
santifer merged 5 commits into
santifer:mainfrom
Schlaflied:feat/pay-transparency-signals
Aug 10, 2026
Merged

feat(oferta): pay-transparency disclosure signals — jurisdiction table + range-width cap (strong) + missing-range (corroborating-only)#2021
santifer merged 5 commits into
santifer:mainfrom
Schlaflied:feat/pay-transparency-signals

Conversation

@Schlaflied

@Schlaflied Schlaflied commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Closes #2019

What this adds

Prompt/instruction + data-template PR — no new executable script. Third member of the statutory-anchor series (#2013 rejection latency, #2018 prohibited content): a generic Block G engine + jurisdiction table, seeded narrow and verified, community-extended.

1. templates/pay-transparency.yml (new system-layer data template)

Jurisdiction table with schema {range_required, width_cap {amount, currency, period}, exemptions, legal_basis, effective, sources}. Seeded with exactly one verified row:

  • CA-ON — publicly advertised postings (employers 25+ employees) must include expected compensation or a range; advertised range width capped at $50,000 CAD/year; requirement does not apply when expected compensation or the top of the range exceeds $200,000/year. Legal basis: Ontario ESA amendments — Working for Workers Acts, 2024 (as implemented by O. Reg. 476/24), effective 2026-01-01. Sources: Fasken, Littler, Hicks Morley, Hyde HR Law bulletins.

Seed-source discipline (documented in the file header): no entry without a citable legal source + explicit effective date. Candidate rows (US-CO 2021, US-NYC 2022, US-CA/US-WA 2023, US-IL 2025, EU Directive 2023/970 transpositions) are listed in comments as contributor suggestions, not as entries.

2. modes/oferta.md — Block G Signal 9, two sub-signals of different evidence strength

Mirrors the strong-vs-corroborating split the terminology-mismatch signal (#1936) established:

  • 9a. Over-wide advertised range — STRONG, presence-based. The agent does the trivial width arithmetic on the advertised_comp it already parses and compares against the table's width_cap. The evidence is in the JD text itself; aggregator re-posting can't manufacture it.
  • 9b. Missing compensation information — CORROBORATING-ONLY, absence-based. Documented as never firing standalone: aggregators strip compliance blocks, and lawful exemptions exist (employer-size thresholds, Ontario's >$200K carve-out). It only attaches to another Block G signal that is already Concerning — exactly the "PTO"/bare-"ESA" precedent.

Jurisdiction resolved from the JD's stated location with config/profile.ymllocation fallback — same region-aware pattern as the employment-classification signal. No-row jurisdictions skip the signal entirely.

Warn-only guarantees

  • Phrasing discipline (pinned by test): report templates state observable facts with the table's citation — "this range is $90K wide; Ontario's O. Reg. 476/24 caps advertised ranges at $50K for covered postings" — and never assert "breaking the law" / "illegal" / "violation". Employer headcount, exemptions, and posting provenance aren't verifiable from a JD. Explicit not-legal-advice note included.
  • Never changes the High Confidence / Proceed with Caution / Suspicious tier on its own; never blocks an evaluation or application; no auto-edits to any user file.
  • All new human-facing prose uses the [Render in {language.output}: ...] localization pattern; data-format tokens stay fixed.

Signal-numbering coordination note

This PR numbers the section Signal 9, immediately after what main currently carries (Signals 1–8, ending with the #1936 terminology-mismatch signal). Two in-flight siblings may collide on that slot: PR #1938 (location-tag mismatch, currently also numbered 9) and the upcoming PR for #2018 (jurisdiction-prohibited content). Same convention as the #1936/#1938 note: whichever merges later renumbers — happy to rebase and renumber this one depending on merge order.

SYSTEM_PATHS

templates/pay-transparency.yml is covered by the existing templates/ directory entry in update-system.mjs SYSTEM_PATHS — verified via node validate-system-paths-coverage.mjs (752 tracked files covered, including the new template).

Test evidence

Three new doc-structure pins in test-all.mjs:

  1. templates/pay-transparency.yml exists, parses as YAML, and the CA-ON seed carries range_required / width_cap (50000 CAD) / exemptions / legal_basis (O. Reg. 476/24) / effective / sources.
  2. oferta.md contains the dual sub-signal section: table reference, 9a strong + 9b corroborating-only, the never-standalone constraint, phrasing discipline, not-legal-advice note.
  3. Phrasing-discipline scan: the report-facing blockquote templates in the new section contain no "illegal" / "violation" / "breaking the law" assertions.

node test-all.mjs --quick: 1801 passed, 0 failed (baseline before this PR: 1798 passed, 0 failed — +3 pins, nothing broken). Same pre-existing warning as baseline.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a pay-transparency range-width check for explicitly stated compensation ranges.
    • The check normalizes periods, flags unusually wide ranges, and presents results as a general arithmetic heuristic—not legal or jurisdiction-specific advice.
    • Incomplete or ambiguous currency, time period, or range values are skipped.
    • Results remain observational and do not affect legitimacy-tier scoring.
  • Improvements

    • Enhanced validation of jurisdiction data, including keyed records and freshness checks.

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The changes replace jurisdiction-table pay-transparency validation with a self-computed compensation-range heuristic. They also extend freshness checks to discover and validate mapping-shaped jurisdiction rows.

Changes

Pay-transparency range-width heuristic

Layer / File(s) Summary
Range-width signal and validation
modes/oferta.md, test-all.mjs
Signal 13 normalizes explicit compensation ranges, skips ambiguous data, and flags widths above half the lower bound. Tests enforce table-free, general-heuristic, and non-legal phrasing.

Mapping-shaped freshness extraction

Layer / File(s) Summary
Mapping row discovery and freshness tests
check-table-freshness.mjs
Freshness checks now inspect qualified object mappings, synthesize missing jurisdictions from keys, and report stale mapped rows.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: santifer, scott-emberson, luochen211

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR does not implement the jurisdiction-aware table, legal width caps, or corroborating missing-range signal required by issue [#2019]. Implement the jurisdiction-aware table and both required sub-signals, or update issue [#2019] to document the narrowed table-free scope.
Out of Scope Changes check ⚠️ Warning The freshness-check redesign and unrelated phrasing-test delimiter fix extend beyond the pay-transparency objectives in issue [#2019]. Remove unrelated freshness-check behavior changes and the unrelated phrasing-test fix, or move them to separate pull requests.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title identifies the pay-transparency range-width feature, but it also names removed jurisdiction-table and missing-range designs.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@santifer

santifer commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Direction settled, @Schlaflied: #2280 has the full reasoning. Sorry for the ten-day wait.

(Reposting this: my first attempt lost three code references to a shell-quoting mistake on my end, and the sentence saying what to remove came out unreadable. Corrected below.)

Sub-signal 9a lands. The table goes.

9a computes the width of an advertised range from the posting itself. That's arithmetic on text the mode already parses, and it's the part with the insight: "this range is unusually wide, which often means the band for your level is undecided or the posting is templated — ask the recruiter for the real band." No statute needed for any of that.

What goes is templates/pay-transparency.yml — specifically width_cap: 50000, range_required: true, and the $200,000 exemption. Those are regulatory parameters: I can't verify them, and I won't notice when they change.

Sub-signal 9b (missing compensation in a jurisdiction whose row sets range_required) goes with the table. Worth noting you'd already reasoned yourself most of the way there: you made 9b corroborating-only precisely because absence is weak evidence that aggregators manufacture. Without the table it has no trigger left.

One thing I want on record because this is the best-argued PR of the ten: the phrasing discipline you wrote is the reason I trust the rest of your work. "Never render this as the employer breaking the law", the carve-outs modelled explicitly, the strong-vs-corroborating split. That's not a promise in a PR description, it's written into the mode where the agent actually reads it. Keep doing that.

…e + range-width cap + corroborating missing-range (closes santifer#2019)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Schlaflied
Schlaflied force-pushed the feat/pay-transparency-signals branch from 1500d1a to bbc65b5 Compare August 6, 2026 00:10
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@modes/oferta.md`:
- Line 404: Update the Signal 13 identifiers consistently: in modes/oferta.md at
lines 404-404, replace 9a and 9b with 13a and 13b; in test-all.mjs at lines
2476-2476, update the failure diagnostic to report 13a and 13b.
- Around line 400-402: Update the corroboration sentence in the
missing-compensation guidance to reference the resolved posting jurisdiction via
{jurisdiction_name}, not “the candidate's jurisdiction.” Only describe it as the
candidate's jurisdiction when the profile location supplied the fallback
resolution.
- Around line 390-394: Gate the advertised compensation width signal in the
jurisdiction-table lookup by requiring a reliable posting date on or after the
matched row’s effective date; when the posting date is unavailable, skip the
signal rather than treating the rule as active. Preserve the existing behavior
for jurisdictions without a table row and use the parsed advertised_comp values
only after this effective-date check.
- Around line 394-396: Update the 13a over-wide-range condition to require a
non-null width_cap plus explicit, unambiguous currencies and periods for both
advertised_comp bounds and the cap, with matching currency and normalized period
before subtracting. Skip 13a when any value is missing, ambiguous, or
incompatible, and do not invent or compare incompatible cap values.

In `@templates/pay-transparency.yml`:
- Around line 20-30: Update the pay-transparency freshness contract so
jurisdiction mapping entries under jurisdictions, including CA-ON, are checked
by check-table-freshness.mjs. Add the required as_of metadata and configure an
adapter/freshness entry for mapping-shaped rows, or define and enforce a
separate contract covering these entries.

In `@test-all.mjs`:
- Around line 2443-2452: Strengthen the seed check condition around the existing
`on` validation: require `on.width_cap?.period` to equal `'year'`, validate
`on.effective` against the expected valid effective date rather than merely
checking truthiness, and ensure both `on.exemptions` and `on.sources` are
non-empty arrays whose entries are strings.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e9fede98-4bf2-412a-b822-546138c7fa5e

📥 Commits

Reviewing files that changed from the base of the PR and between cf0d011 and bbc65b5.

📒 Files selected for processing (3)
  • modes/oferta.md
  • templates/pay-transparency.yml
  • test-all.mjs

Comment thread modes/oferta.md Outdated
Comment thread modes/oferta.md Outdated
Comment thread modes/oferta.md Outdated
Comment thread modes/oferta.md Outdated
Comment thread templates/pay-transparency.yml Outdated
Comment thread test-all.mjs Outdated
…ration text, effective-date gating, freshness coverage, stronger seed test (CodeRabbit)

- modes/oferta.md: fix leftover 9a/9b -> 13a/13b in the closing paragraph
  (rebase renumbered the signal but missed this reference); gate 13a on a
  reliable posting date on/after the jurisdiction row's `effective` date
  (skip if unavailable); require a non-null width_cap plus matching,
  unambiguous currency/period on both advertised_comp and width_cap before
  any arithmetic; use {jurisdiction_name} (resolved posting jurisdiction)
  in the 13b corroboration sentence instead of always saying "the
  candidate's jurisdiction"
- test-all.mjs: fix matching 9a/9b -> 13a/13b in the failure diagnostic;
  strengthen the CA-ON pay-transparency seed self-test to require
  width_cap.period === 'year', the exact expected effective date
  (2026-01-01), and non-empty string-array exemptions/sources
- templates/pay-transparency.yml: add `as_of` to the CA-ON row so
  check-table-freshness.mjs's freshness contract actually covers it
- check-table-freshness.mjs: extend extractRows() to also discover
  mapping-shaped row-sets (object keyed by jurisdiction code, e.g.
  `jurisdictions: { CA-ON: {...} }`) alongside the existing array-shaped
  ones, synthesizing `jurisdiction` from the map key; adds self-test
  coverage for the new shape

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test-all.mjs`:
- Around line 2443-2458: Update the CA-ON seed validation condition around
isNonEmptyStringArray and effectiveStr to require on.as_of as a quoted
YYYY-MM-DD date, matching the discovery contract used by
check-table-freshness.mjs. Include the as_of requirement in the success
assertion and expand the failure message so missing or invalid freshness
metadata is reported.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 932bfda8-a79f-42ae-9684-4be2492d8ed6

📥 Commits

Reviewing files that changed from the base of the PR and between bbc65b5 and 4a412e4.

📒 Files selected for processing (4)
  • check-table-freshness.mjs
  • modes/oferta.md
  • templates/pay-transparency.yml
  • test-all.mjs

Comment thread test-all.mjs Outdated
Schlaflied and others added 2 commits August 6, 2026 00:20
…ching freshness-check contract (CodeRabbit)

The CA-ON seed self-test validated width_cap/exemptions/legal_basis/effective/sources
but never checked as_of, the field check-table-freshness.mjs actually uses to discover
and validate the row. Add a strict string + YYYY-MM-DD format check for on.as_of to the
success assertion, and expand the failure message to report the field's actual value and
type (flagging an unquoted-Date case explicitly) when it's missing or malformed.

templates/pay-transparency.yml's CA-ON row already quotes as_of ("2026-07-18"), so no
data change was needed -- only the test was under-validating it.
…rection, keep only self-computed range-width signal (santifer#2280)

santifer's direction on santifer#2021: sub-signal 9a/13a (the range-width
arithmetic computed purely from the posting's own advertised_comp)
lands, but templates/pay-transparency.yml goes entirely -- width_cap,
range_required, and the $200K exemption are regulatory parameters he
can't verify and won't notice change. Sub-signal 9b/13b (missing-comp
corroboration keyed to a jurisdiction's range_required row) had no
trigger left without the table, so it goes with it.

- modes/oferta.md: replace the two-sub-signal, table-backed Block G
  signal 13 with a single table-free "Pay-Transparency Range-Width
  Check". Heuristic is now a generic ratio (width > 0.5x the range's
  own floor) applied to any posting in any jurisdiction, explicitly
  framed in the mode text as a general heuristic, not a legal
  threshold. Preserves the phrasing-discipline pattern santifer
  praised (never asserting the employer broke the law).
- templates/pay-transparency.yml: deleted.
- check-table-freshness.mjs: reworded the two comments that named the
  deleted file; kept the generic mapping-shaped table discovery path
  since it's schema-agnostic, self-tested with synthetic fixtures, and
  has no dependency on this specific file.
- test-all.mjs: replaced the table-parsing/dual-sub-signal tests with
  a table-removed assertion + a single table-free signal check; fixed
  a stale string-slice delimiter in the unrelated jurisdiction-
  prohibited-content phrasing test that referenced the old heading
  text.

templates/README.md and AGENTS.md/docs/SCRIPTS.md never had a row/
reference for this file, so nothing to remove there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@modes/oferta.md`:
- Around line 390-392: Update the compensation-range requirements in the signal
description to require both normalized bounds to use the same currency and the
lower bound to be greater than zero. State that ranges with mismatched
currencies or a non-positive normalized lower bound must skip the signal, while
preserving the existing normalization and width comparison rules.

In `@test-all.mjs`:
- Around line 2450-2456: Strengthen the condition in the test block around
ptSection so it also structurally asserts the documented threshold formula “top
- bottom > 0.5 × bottom.” Match the relevant expression or equivalent threshold
wording in ptSection alongside the existing framing checks, without removing the
current assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 279c3e13-275f-4509-aad7-094acf926255

📥 Commits

Reviewing files that changed from the base of the PR and between 6ff0486 and 15a4466.

📒 Files selected for processing (3)
  • check-table-freshness.mjs
  • modes/oferta.md
  • test-all.mjs

Comment thread modes/oferta.md Outdated
Comment thread test-all.mjs
…nd, structurally assert threshold formula in tests (CodeRabbit)

- modes/oferta.md: signal 13 now explicitly requires the two normalized
  compensation bounds to share the same currency and the normalized lower
  bound to be strictly positive; mismatched currency or a zero/negative
  floor skips the signal entirely instead of computing it.
- test-all.mjs: the Block G pay-transparency test now also asserts the
  documented threshold formula ("top - bottom > 0.5 × bottom") is present
  in ptSection, in addition to the existing framing/phrasing checks.
@Schlaflied

Copy link
Copy Markdown
Contributor Author

@santifer Reworked per your direction in #2280templates/pay-transparency.yml dropped entirely; kept only the range-width signal (renamed to a standalone check), computed purely from the posting's own advertised_comp numbers with no jurisdiction table involved. Also cleared two follow-up CodeRabbit rounds since (currency-match + positive-lower-bound gating, and a structural test for the threshold formula). CI green, mergeable.

@santifer
santifer merged commit 9482765 into santifer:main Aug 10, 2026
13 checks passed
@santifer

Copy link
Copy Markdown
Owner

Another one landed exactly per direction, @Schlaflied. Dropping the jurisdiction table and keeping only the self-computed range-width arithmetic — plus a CI assertion that fails if templates/pay-transparency.yml ever reappears — is precisely the shape #2280 asked for: signals the repo can verify from the posting's own numbers, never live legal figures we can't maintain. Merged! 🚀

@github-actions github-actions Bot mentioned this pull request Aug 10, 2026
santifer added a commit to Schlaflied/career-ops that referenced this pull request Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(oferta): pay-transparency disclosure signals — jurisdiction-aware range-width cap (strong) + missing-range (corroborating-only)

2 participants