126 lines
6.7 KiB
Markdown
126 lines
6.7 KiB
Markdown
# Mode: titles — Adjacent Job-Title Suggestions
|
||
|
||
## Purpose
|
||
|
||
The scanner only surfaces what `portals.yml` `title_filter.positive` matches —
|
||
and that list is written from the titles the user already knows to search for.
|
||
The same job ships under many names (Solutions Architect / Forward Deployed
|
||
Engineer / Customer Engineer), so the search is silently narrower than the CV
|
||
justifies. This mode reads the CV and proposes adjacent titles the user isn't
|
||
searching for yet — then, only after explicit confirmation, writes the accepted
|
||
keywords into `title_filter.positive` so the very next `scan` casts the wider net.
|
||
|
||
`patterns` Step 1b makes the same kind of retargeting recommendation
|
||
("consider adding archetype X and reweighting `portals.yml`
|
||
`title_filter.positive`"), but only after ≥5 progressed applications and only
|
||
from interview-session signal. This mode is the day-zero, CV-driven complement.
|
||
It is also the inverse of `upskill`: upskill finds skills missing for current
|
||
targets; this finds new targets reachable from current skills.
|
||
|
||
## Inputs
|
||
|
||
- `cv.md` — the **only** source of evidence for suggestions (required)
|
||
- `config/profile.yml` — `archetypes` (name / level / fit) for what's already targeted
|
||
- `modes/_profile.md` — target roles, framing, and any deal-breakers the user has recorded
|
||
- `portals.yml` — the current `title_filter.positive` (and `negative`) keywords
|
||
- Optional: if `data/applications.md` has ≥5 entries progressed beyond
|
||
`Evaluated`, note which suggestions the outcome data supports (e.g. an axis
|
||
that is already converting) — cross-reference `patterns` rather than
|
||
duplicating its analysis.
|
||
|
||
## The Three Axes
|
||
|
||
Classify every suggestion on exactly one axis, and say which:
|
||
|
||
- **Lateral** — same work, different label. The core recall win: the user
|
||
already does this job; the market just posts it under a name they don't
|
||
search for.
|
||
- **Stretch** — one level up or larger scope than the CV's strongest evidence.
|
||
Plausible, but a hiring manager would probe the gap.
|
||
- **Pivot** — an adjacent function reachable from existing CV evidence
|
||
(e.g. heavy client-facing delivery work → pre-sales engineering).
|
||
|
||
## Output Contract (per suggestion)
|
||
|
||
For each suggested title, show exactly:
|
||
|
||
- **Title** — the market title as actually posted, not an invented hybrid
|
||
- **Axis** — Lateral / Stretch / Pivot
|
||
- **CV evidence** — 1–2 lines from `cv.md` **quoted verbatim**. If you cannot
|
||
quote it, do not suggest it.
|
||
- **Honest gap note** — what a hiring manager would question; "none" is allowed
|
||
for Lateral suggestions but must be earned
|
||
- **Market-reality note** — how common the title is, where it tends to be
|
||
posted, seniority skew, or noise level
|
||
|
||
Aim for 5–10 suggestions, Lateral first. Fewer good suggestions beat a padded
|
||
list — this system optimizes for quality, not quantity.
|
||
|
||
## Filters (apply BEFORE showing suggestions)
|
||
|
||
1. **Dedup against existing coverage.** Mirror the matcher semantics in
|
||
`scan.mjs` (`buildTitleFilter` / `compileKeyword`): the scanner lowercases
|
||
both sides and keeps a job when any positive keyword is a
|
||
case-insensitive substring of the title (2–3 letter keywords match on word
|
||
boundaries instead). So drop any candidate title that an existing positive
|
||
keyword already substring-matches — it is already covered, and suggesting
|
||
it adds zero new recall.
|
||
2. **Deal-breaker filter.** Never suggest titles that violate the
|
||
deal-breakers recorded in `modes/_profile.md` (e.g. "no people management"
|
||
rules out Engineering Manager; "no on-site" rules out field roles). Titles
|
||
matching `title_filter.negative` keywords are also off the table — the user
|
||
already excluded them.
|
||
3. **Never invent experience.** Every suggestion must be traceable to quoted
|
||
`cv.md` lines — the source-of-truth boundary applies to suggestions exactly
|
||
as it does to CV content. Keywords get reformulated, never fabricated. If
|
||
the evidence isn't in `cv.md`, ask the user; don't stretch a quote to fit.
|
||
|
||
## Confirm Gate — Writing Accepted Titles (HARD RULE)
|
||
|
||
When the user accepts one or more suggestions:
|
||
|
||
1. Derive **keywords, not raw titles**. The filter matches substrings, so the
|
||
keyword should be the shortest phrase that still identifies the role family
|
||
("Forward Deployed" covers Forward Deployed Engineer/Architect/Lead).
|
||
2. Attach a **breadth warning** to any substring-dangerous keyword: because
|
||
matching is substring-based, a short or generic keyword floods the scan.
|
||
Propose "Solutions Architect", never bare "Architect" — bare "Architect"
|
||
would also match Data Architect, Enterprise Architect, Security Architect.
|
||
If the user insists on a broad keyword, warn once and comply.
|
||
3. Skip keywords that duplicate existing coverage (same dedup rule as above);
|
||
preserve the casing style already used in the user's `portals.yml`.
|
||
4. Show the **exact YAML diff** against `portals.yml` `title_filter.positive`
|
||
before touching anything.
|
||
5. **Never write to `portals.yml` without explicit user confirmation.**
|
||
"Show me the diff" is not a yes. Silence is not a yes.
|
||
6. `portals.yml` (user layer) is **the only file this mode writes by
|
||
default**. This mode proposes no negative keywords — precision guards for
|
||
noisy keywords are deferred to #1353's seniority-tier helper.
|
||
7. **Separately-confirmed exception:** accepted titles can additionally become
|
||
`fit: adjacent` archetypes in `config/profile.yml` (an existing schema
|
||
field — see `config/profile.example.yml`). Mention that this is possible,
|
||
but do it **only if the user asks** — never write archetypes by default.
|
||
When the user does ask, that write gets its **own YAML diff and its own
|
||
separate confirmation**; never bundle the `portals.yml` and
|
||
`config/profile.yml` writes into one confirmation.
|
||
|
||
## After the Write
|
||
|
||
- Suggest `/career-ops scan` — the wider filter only pays off on the next scan.
|
||
- Suggest `upskill` scoped to a Stretch title the user liked, to see the gap
|
||
map between the CV and that next-level target.
|
||
|
||
## Error Handling
|
||
|
||
- `cv.md` missing → stop and point at onboarding (`node doctor.mjs --json`).
|
||
There is no evidence base to suggest from, and inventing one is forbidden.
|
||
- `portals.yml` missing, or `title_filter.positive` empty → offer to create it
|
||
from `templates/portals.example.yml` first, then re-run this mode. (An empty
|
||
positive list means the scanner matches everything — nothing to broaden.)
|
||
- `config/profile.yml` or `modes/_profile.md` missing → **hard stop**: do not
|
||
generate suggestions. Point at onboarding (`node doctor.mjs --json`) and
|
||
stop, then re-run this mode once both files exist — the same
|
||
fix-first-then-re-run behavior as a missing `portals.yml` above.
|
||
Deal-breakers live in `modes/_profile.md` — suggestions generated without
|
||
them can propose exactly what the user excluded.
|