Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pipelines/job-assessment/assessment/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,8 @@ every top-level key present, nothing extra.
"autonomy": {"net": "positive", "quotes": ["You own the docs platform end to end"],
"read": "You would decide how the platform works."},
"pay": {"stated": true, "tier_used": null, "top": null, "other_pay_noted": [],
"tiers": [{"label": "all other US locations", "min": 120000, "max": 150000,
"quote": "Base pay range: $120,000 - $150,000 (all other US locations)"}]},
"tiers": [{"label": "all other US locations", "min": 98000, "max": 104000,
"quote": "Base pay range: $98,000 - $104,000 (all other US locations)"}]},
"culture": {"perks": [{"perk_id": "learning_budget", "quote": "Annual learning budget"}],
"strong_positive_phrases": [], "low_time_off": null, "hustle": [],
"soft_flags_tripped": [], "negative_phrases": []},
Expand Down
65 changes: 52 additions & 13 deletions pipelines/job-assessment/assessment/scripts/check_findings.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,30 @@ def quote_ok(path, quote):
return False
return True

def objects(value, path):
"""The object entries of a list. A non-list or a non-object entry is a problem."""
if value is None:
return []
if not isinstance(value, list):
add(path, "must be a list.")
return []
kept = []
for i, entry in enumerate(value):
if isinstance(entry, dict):
kept.append((i, entry))
else:
add(f"{path}[{i}]", "must be an object, not a plain value such as a string.")
return kept

def section(value, path):
"""A findings section that must be an object. Anything else counts as empty."""
if value in (None, {}, [], ""):
return {}
if not isinstance(value, dict):
add(path, "must be an object.")
return {}
return value

if not isinstance(findings, dict):
return ["findings: the file is not a JSON object."]

Expand All @@ -131,7 +155,7 @@ def quote_ok(path, quote):
requirement_ids = {r.get("id") for r in lane.get("requirements") or []}

# Hard block.
block = findings.get("hard_block") or {}
block = section(findings.get("hard_block"), "hard_block")
if block.get("tripped"):
if block.get("id") not in blocks:
add("hard_block.id", f'"{block.get("id")}" is not a hard block in the profile.')
Expand All @@ -141,18 +165,21 @@ def quote_ok(path, quote):
f'no named exception for "{block.get("id")}" exists in the profile.')

# Job-type override.
override = findings.get("job_type_override") or {}
override = section(findings.get("job_type_override"), "job_type_override")
if override.get("fired"):
if not override.get("skill"):
add("job_type_override.skill", "fired but names no skill.")
quotes = override.get("quotes") or []
if not quotes:
if not isinstance(quotes, list):
add("job_type_override.quotes", "must be a list.")
quotes = []
elif not quotes:
add("job_type_override.quotes", "fired but quotes nothing from the posting.")
for i, q in enumerate(quotes):
quote_ok(f"job_type_override.quotes[{i}]", q)

# Requirement ratings. Silence is Unknown. Anything else needs a quote.
for i, item in enumerate(findings.get("requirements") or []):
for i, item in objects(findings.get("requirements"), "requirements"):
path = f"requirements[{i}]"
rating = str(item.get("rating", "")).lower()
if rating not in RATINGS:
Expand All @@ -170,24 +197,27 @@ def quote_ok(path, quote):
"a read on autonomy rests only on generic phrases. The posting is silent, so rate it unknown.")

# Autonomy read.
autonomy = findings.get("autonomy") or {}
autonomy = section(findings.get("autonomy"), "autonomy")
if autonomy.get("net") in ("negative", "mixed"):
quotes = autonomy.get("quotes") or []
if not quotes:
if not isinstance(quotes, list):
add("autonomy.quotes", "must be a list.")
quotes = []
elif not quotes:
add("autonomy.quotes", "a read on autonomy quotes nothing from the posting.")
found = [q for i, q in enumerate(quotes) if quote_ok(f"autonomy.quotes[{i}]", q)]
if found and all(_generic_only(q) for q in found):
add("autonomy.quotes",
"the autonomy read rests only on generic phrases. The posting is silent, so it is unknown, not negative.")

# Culture: every change needs a quote.
culture = findings.get("culture") or {}
for i, perk in enumerate(culture.get("perks") or []):
culture = section(findings.get("culture"), "culture")
for i, perk in objects(culture.get("perks"), "culture.perks"):
if perk.get("perk_id") not in perks:
add(f"culture.perks[{i}].perk_id", f'"{perk.get("perk_id")}" is not a perk in the profile.')
quote_ok(f"culture.perks[{i}].quote", perk.get("quote"))
for key in ("strong_positive_phrases", "hustle", "negative_phrases", "soft_flags_tripped"):
for i, entry in enumerate(culture.get(key) or []):
for i, entry in objects(culture.get(key), f"culture.{key}"):
quote_ok(f"culture.{key}[{i}].quote", entry.get("quote"))
low = culture.get("low_time_off")
if isinstance(low, dict):
Expand All @@ -196,11 +226,14 @@ def quote_ok(path, quote):
add("culture.low_time_off", "has no quote from the posting.")

# Qualifications: every claim points at real, usable evidence.
qual = findings.get("qualifications") or {}
for i, match in enumerate(qual.get("matches") or []):
qual = section(findings.get("qualifications"), "qualifications")
for i, match in objects(qual.get("matches"), "qualifications.matches"):
path = f"qualifications.matches[{i}]"
quote_ok(f"{path}.requirement_quote", match.get("requirement_quote"))
ids = match.get("evidence_ids") or []
if not isinstance(ids, list):
add(f"{path}.evidence_ids", "must be a list of evidence ids.")
ids = []
if not ids:
add(f"{path}.evidence_ids", "a match with no evidence behind it. Write it as unproven instead.")
for ev_id in ids:
Expand All @@ -211,7 +244,7 @@ def quote_ok(path, quote):
add(f"{path}.evidence_ids", f'"{ev_id}" is marked do_not_use and cannot back a claim.')
elif entry.get("authorship") == "OTHER-AUTHOR":
add(f"{path}.evidence_ids", f'"{ev_id}" is work someone else wrote and cannot back a claim.')
for i, item in enumerate(qual.get("unproven") or []):
for i, item in objects(qual.get("unproven"), "qualifications.unproven"):
path = f"qualifications.unproven[{i}]"
if item.get("skill_id") not in skills:
add(f"{path}.skill_id", f'"{item.get("skill_id")}" is not a skill in the profile.')
Expand Down Expand Up @@ -245,7 +278,13 @@ def main(argv=None):
print(f"check_findings: could not read inputs: {exc}", file=sys.stderr)
return 2
meta, text = split_posting(raw)
problems = check(findings, text, meta.get("lane"), profile)
try:
problems = check(findings, text, meta.get("lane"), profile)
except (AttributeError, TypeError, KeyError) as exc:
# A shape this script does not know how to name. Still a bad findings
# file, so say so in one line and stop with the "problems found" code.
print(f"check_findings: the findings file has a shape this check cannot read ({exc}).")
return 1
for line in problems:
print(line)
if problems:
Expand Down
10 changes: 5 additions & 5 deletions pipelines/job-assessment/fixtures/broken-profile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ hard_blocks:
match_hints: [sports betting, casino]
comp:
currency: USD
floor: 90000
min: 130000
open_ask: 125000
target: 140000
stretch_ceiling: 170000
floor: 75000
min: 98000
open_ask: 85000
target: 100000
stretch_ceiling: 175000
culture:
perks:
- {id: unlimited_pto, label: Unlimited PTO, kind: big}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,19 @@
"tiers": [
{
"label": "Region B",
"min": 125000,
"max": 150000,
"quote": "Base pay range for Region B: $125,000 to $150,000."
"min": 100000,
"max": 104000,
"quote": "Base pay range for Region B: $100,000 to $104,000."
},
{
"label": "Region A",
"min": 140000,
"max": 170000,
"quote": "Region A: $140,000 to $170,000."
"min": 102000,
"max": 118000,
"quote": "Region A: $102,000 to $118,000."
}
],
"tier_used": "Region B",
"top": 150000,
"top": 104000,
"other_pay_noted": []
},
"culture": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,19 @@
"tiers": [
{
"label": "Region A",
"min": 105000,
"max": 120000,
"quote": "Region A: $105,000 to $120,000."
"min": 82000,
"max": 98000,
"quote": "Region A: $82,000 to $98,000."
},
{
"label": "Everywhere else",
"min": 85000,
"max": 100000,
"quote": "Everywhere else: $85,000 to $100,000."
"min": 72000,
"max": 78000,
"quote": "Everywhere else: $72,000 to $78,000."
}
],
"tier_used": "Everywhere else",
"top": 100000,
"top": 78000,
"other_pay_noted": []
},
"culture": {
Expand Down
10 changes: 5 additions & 5 deletions pipelines/job-assessment/fixtures/gappy-profile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ named_exceptions:
added: 2026-10-01
comp:
currency: USD
floor: 90000
min: 110000
open_ask: 125000
target: 140000
stretch_ceiling: 170000
floor: 75000
min: 80000
open_ask: 85000
target: 100000
stretch_ceiling: 175000
culture:
perks:
- {id: unlimited_pto, label: Unlimited PTO, kind: big}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@
<div style="font-size:12px; font-weight:600; color:#9ca3af; margin-top:2px;">&#9679; Unknown</div>
</td>
<td style="padding:10px 0 10px 12px; border-top:1px solid #f3f4f6; vertical-align:top;">
<div style="font-size:13px; line-height:1.5; color:#4b5563;">The posting lists base pay up to 150000.</div>
<div style="font-size:13px; line-height:1.5; color:#4b5563;">The posting lists base pay up to 104000.</div>
</td>
</tr>
</table>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ Copperline makes writing tools for software teams. We are a remote company with

## Pay and benefits

- Base pay range for Region B: $125,000 to $150,000. Region A: $140,000 to $170,000.
- Base pay range for Region B: $100,000 to $104,000. Region A: $102,000 to $118,000.
- Unlimited paid time off, with a three-week minimum we ask everyone to take.
- A $3,000 yearly learning budget.
- Two team meetups a year.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
<div style="font-size:12px; font-weight:600; color:#9ca3af; margin-top:2px;">&#9679; Unknown</div>
</td>
<td style="padding:10px 0 10px 12px; border-top:1px solid #f3f4f6; vertical-align:top;">
<div style="font-size:13px; line-height:1.5; color:#4b5563;">The posting lists base pay up to 100000.</div>
<div style="font-size:13px; line-height:1.5; color:#4b5563;">The posting lists base pay up to 78000.</div>
</td>
</tr>
</table>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ The split follows Modestino, Shoag and Ballance (*Review of Economics and Statis
| 12+ years of technical writing experience. | experience | likely filter |
| Fluent in DITA XML and a component content management system. | skill | likely real |
| Experience documenting iOS and Android SDKs. | skill | likely real |
| Region A: $105,000 to $120,000. | skill | likely real |
| Everywhere else: $85,000 to $100,000. | skill | likely real |
| Region A: $82,000 to $98,000. | skill | likely real |
| Everywhere else: $72,000 to $78,000. | skill | likely real |

---

Expand Down Expand Up @@ -142,6 +142,6 @@ Driftmark sells training courses for a software product. We are a friendly team

## Pay

- Region A: $105,000 to $120,000.
- Everywhere else: $85,000 to $100,000.
- Region A: $82,000 to $98,000.
- Everywhere else: $72,000 to $78,000.

Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Copperline makes writing tools for software teams. We are a remote company with

## Pay and benefits

- Base pay range for Region B: $125,000 to $150,000. Region A: $140,000 to $170,000.
- Base pay range for Region B: $100,000 to $104,000. Region A: $102,000 to $118,000.
- Unlimited paid time off, with a three-week minimum we ask everyone to take.
- A $3,000 yearly learning budget.
- Two team meetups a year.
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ Driftmark sells training courses for a software product. We are a friendly team

## Pay

- Region A: $105,000 to $120,000.
- Everywhere else: $85,000 to $100,000.
- Region A: $82,000 to $98,000.
- Everywhere else: $72,000 to $78,000.
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ named_exceptions:
- {company: Lanternfield Example Co., block_id: non_remote, why: "Robin lives a ten minute walk from this office and would go in.", added: "2026-10-01"}
comp:
currency: USD
floor: 90000
min: 110000
open_ask: 125000
target: 140000
stretch_ceiling: 170000
floor: 75000
min: 80000
open_ask: 85000
target: 100000
stretch_ceiling: 175000
culture:
perks:
- {id: unlimited_pto, label: Unlimited PTO, kind: big}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ s3.localization_pm: 2, would rather avoid, within 5 years, self, no evidence
# Stage 4. Needs
s4.q1: Gambling and betting, weapons makers, and any job that is not remote.
s4.q2: Yes, Lanternfield Example Co. I live a ten minute walk from its office.
s4.q3: 90000 US dollars.
s4.q4: 110000.
s4.q5: 125000.
s4.q6: 140000.
s4.q7: 170000.
s4.q3: 75000 US dollars.
s4.q4: 80000.
s4.q5: 85000.
s4.q6: 100000.
s4.q7: 175000.
s4.q8: Region B.
s4.q9: Unlimited time off, a learning budget and extra paid days off matter most. Meetups, a home-office stipend and paid parental leave are nice.
s4.q10: Fewer than 15 days a year.
Expand Down
2 changes: 1 addition & 1 deletion pipelines/job-assessment/intake/stages/04-needs.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ YAML
```bash
python3 scripts/add_entry.py PROFILE --section comp --entry - <<'YAML'
currency: USD
floor: 90000
floor: 75000
YAML
```

Expand Down
6 changes: 4 additions & 2 deletions pipelines/job-assessment/tests/e2e-output-run1.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<!-- FICTIONAL EXAMPLE DATA. Not a real person. Saved by tests/live/test_live_assessment.py -->
# Live run, first attempt (posting 03 FAILED, kept as evidence of model variance)

Note: the pay figures in this record were changed after the run to fictional ones. Nothing else was edited; the scores and verdicts are as recorded.

The second attempt, which passed, is in `e2e-output.md`. Nothing was changed between the two runs.

Date: 2026-10-01. Model: `sonnet`. A model followed `assessment/SKILL.md` and wrote its own findings. The test recomputes scores and the verdict with the scripts and does not trust the model's numbers. Tool output is cut to 500 characters. Folder names are replaced with `<work>` and `<tmp>`.
Expand Down Expand Up @@ -228,7 +230,7 @@ override=v.override_skill(f),hard_block=v.hard_block_id(f))))" > work/verdict.js
"fit": [],
"comp": [
"Tier used: Region B",
"Top of range 150000 against target 140000, min 110000, floor 90000"
"Top of range 104000 against target 100000, min 80000, floor 75000"
],
"qualifications": [
"1 load-bearing gap(s), -1 (cap 4)"
Expand Down Expand Up @@ -312,7 +314,7 @@ Culture 10 🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟

[Open the note](file://<work>/archive/🟢%20📋%20🔧%20Copperline%20Example%20Co.%20-%20Docs%20Platform%20Engineer%20-%202026-10-01.md)

The verdict is Apply, and no earlier rule fired: there was no hard block, no job-type override, and no score below the floor or in the reservations band. Because Copperline is a high-interest company, move fast and check for a warm introduction. The one thing to prepare for is the Go line: the posting wants you to read Go code, and you have no recorded evidence of recent Go work. That cost one Qualifications point. The Fit, Comp and Culture bars are all at 10, and Comp is based on the Region B tier ($125k–$150k).
The verdict is Apply, and no earlier rule fired: there was no hard block, no job-type override, and no score below the floor or in the reservations band. Because Copperline is a high-interest company, move fast and check for a warm introduction. The one thing to prepare for is the Go line: the posting wants you to read Go code, and you have no recorded evidence of recent Go work. That cost one Qualifications point. The Fit, Comp and Culture bars are all at 10, and Comp is based on the Region B tier ($85k–$104k).

The email card is at `out/copperline-example-co-docs-platform-engineer.html`. It was written and passed its own check, and nothing was sent.

Expand Down
4 changes: 3 additions & 1 deletion pipelines/job-assessment/tests/e2e-output.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<!-- FICTIONAL EXAMPLE DATA. Not a real person. Saved by tests/live/test_live_assessment.py -->
# Live run: assessment on the fictional postings

Note: the pay figures in this record were changed after the run to fictional ones. Nothing else was edited; the scores and verdicts are as recorded.

Date: 2026-10-01. Model: `sonnet`. A model followed `assessment/SKILL.md` and wrote its own findings. The test recomputes scores and the verdict with the scripts and does not trust the model's numbers. Tool output is cut to 500 characters. Folder names are replaced with `<work>` and `<tmp>`. One path to a saved tool-output file, which named a home folder, was replaced by hand with `<tool-output-file>` after the run.

## Posting 01-strong-fit
Expand Down Expand Up @@ -242,7 +244,7 @@ check_findings: ok
"fit": [],
"comp": [
"Tier used: Region B",
"Top of range 150000 against target 140000, min 110000, floor 90000"
"Top of range 104000 against target 100000, min 80000, floor 75000"
],
"qualifications": [
"1 load-bearing gap(s), -1 (cap 4)"
Expand Down
Loading
Loading