How `negotiation-roi.mjs` Generates Salary Negotiation Talking Points from Story-Bank Achievements in santifer/career-ops
negotiation-roi.mjs is a pure Node.js utility that transforms quantified STAR achievements from interview-prep/story-bank.md into dollar-valued negotiation talking points through a rigorous verification pipeline that cross-checks every claim against cv.md before calculating ROI.
The santifer/career-ops repository provides a disciplined approach to salary negotiation preparation. Rather than relying on hand-waving or suspect numbers, negotiation-roi.mjs enforces a v1 safety gate: every quantitative claim must appear verbatim in both your story bank and your CV before it can be converted into a credible talking point. This prevents inflated or fabricated figures from entering salary discussions.
The Core Pipeline: From Story to Negotiation Script
The analyze() function (lines 22–73 in negotiation-roi.mjs) orchestrates an 8-step pipeline that guarantees traceable, defensible talking points.
Step 1: Story Parsing
The script imports parseStories() from match-star.mjs to read every STAR-formatted story from interview-prep/story-bank.md. This extraction happens at lines 22–24:
// Inside analyze()
const stories = parseStories(storyBank); // line 22-24
Each story is parsed into structured components: Situation, Task, Action, Result—enabling downstream analysis of the Result section where quantified achievements typically live.
Step 2: Claim Extraction
extractQuantifiedClaims() (lines 97–100) applies two regular expressions to identify measurable impact:
TIME_REDUCTION_RE— captures patterns like "8 hours → 2 hours"PERCENT_REDUCTION_RE— captures patterns like "cut by 60%"
Both patterns extract the before/after values with their units, producing structured claim objects.
Step 3: The Verification Gate (Critical Safety Mechanism)
The verifyClaim() function (lines 91–100) implements the v1 safety gate. It builds all textual variants of a number-unit pair—for example, "8 hours", "8hours", "8-hours"—and checks them against cv.md using a boundary-aware regex:
const boundaryAware = new RegExp(`(?<![\\d.])${escaped}(?![\\d.])`);
This prevents false positives: "8 hours" won't match "18 hours" or "80 hours" because the regex requires a non-digit boundary before and after.
Step 4: Wage Resolution Without Guessing
resolveWage() (lines 32–37) extracts an hourly rate through strict precedence:
- Scan the story text for
$X/hror$X/hourpatterns - Fall back to
--wageCLI flag - Mark uncalculable if neither exists—never default to assumptions
Step 5: Frequency Resolution
resolveFrequency() (lines 14–24) scans for explicit frequency phrases defined in FREQUENCY_TEXT_PATTERNS:
- "daily", "day", "every day"
- "weekly", "week", "every week"
- "monthly", "month", "every month"
- "quarterly", "quarter", "every quarter"
- "yearly", "annual", "per year"
If no pattern matches, it falls back to --frequency or --occurrences flags. Missing frequency data marks the claim uncalculable.
Step 6: ROI Calculation
For verified time-reduction claims, computeCalculation() (lines 64–73) executes transparent arithmetic:
hoursSaved = before_value - after_value
annualValue = hoursSaved × $wage/hr × occurrences_per_year
Step 7: Draft Paragraph Generation
buildDraftParagraph() (lines 5–9) assembles the final talking point with three components:
- The original achievement sentence (verbatim from story bank)
- The estimated value formula showing exact calculation
- A transferability disclaimer acknowledging this is pure arithmetic, not market-optimized pricing
Step 8: Result Aggregation
analyze() returns a structured result containing:
calculableTalkingPoints: ready-to-use paragraphs with dollar valuesuncalculableItems: claims with specific reasons (missing wage, missing frequency, unverified)warnings: edge cases detected during processingcounts: totals of scanned stories and extracted claims
Practical Usage Examples
Extract Claims Programmatically
import { extractQuantifiedClaims } from './negotiation-roi.mjs';
const story = `
I built a template automation that cut a recurring process from 8 hours to 2 hours per batch.
`;
const claims = extractQuantifiedClaims(story, 'Automation Story');
console.log(claims);
/* → [
{
story: 'Automation Story',
type: 'time-reduction',
raw: '8 hours to 2 hours',
sentence: 'cut a recurring process from 8 hours to 2 hours per batch',
before: { value: 8, unit: 'hours' },
after: { value: 2, unit: 'hours' }
}
] */
Verify a Claim Against Your CV
import { verifyClaim } from './negotiation-roi.mjs';
import { readFileSync } from 'fs';
const cv = readFileSync('cv.md', 'utf-8');
const claim = { type: 'time-reduction', before: { value: 8 }, after: { value: 2 } };
const { verified } = verifyClaim(claim, cv);
console.log(verified); // true only if "8 hours" and "2 hours" appear verbatim in cv.md
Full Pipeline Execution
import { analyze } from './negotiation-roi.mjs';
import { readFileSync } from 'fs';
const storyBank = readFileSync('interview-prep/story-bank.md', 'utf-8');
const cv = readFileSync('cv.md', 'utf-8');
const result = analyze(storyBank, cv, { wage: 45, frequency: 'weekly' });
console.log(JSON.stringify(result, null, 2));
CLI Invocation for Quick Summary
node negotiation-roi.mjs --summary --wage 45 --frequency weekly
The --summary flag triggers printSummary() (lines 4–33) for human-readable output without JSON noise.
File Architecture and Dependencies
| File | Role in the negotiation-roi.mjs flow |
|---|---|
negotiation-roi.mjs |
Central engine—extraction, verification, calculation, and drafting |
interview-prep/story-bank.md |
Source repository of quantified STAR achievements |
cv.md |
Ground-truth verification file; claims must appear here to proceed |
match-star.mjs |
STAR format parser converting markdown stories to structured objects |
lib/cli-flags.mjs |
Robust flag handling with clear error messages for missing values |
Why This Approach Works for Salary Negotiation
Anchoring in verified achievements changes the negotiation dynamic. Instead of stating "I think I'm worth $X," you present: "I automated a process that saves 6 hours weekly; at $45/hr, that's $14,040 in recovered capacity annually."
The script's rigid safety mechanisms protect against common pitfalls:
- Scope creep on numbers: The boundary-aware regex prevents "8 hours" from accidentally matching "18 hours"
- Wage inflation assumptions: No embedded "market rate" defaults—wage must be explicit
- Frequency hand-waving: "Regularly" or "often" trigger uncalculable status; explicit frequency required
Summary
negotiation-roi.mjsconverts STAR story achievements into dollar-valued negotiation talking points through a disciplined, verifiable pipeline.- The v1 safety gate (
verifyClaim(), lines 91–100) guarantees every number appears verbatim incv.mdbefore use. - Wage and frequency resolution (
resolveWage()at lines 32–37,resolveFrequency()at lines 14–24) never guess—missing data marks claims uncalculable with clear reasons. - Transparent arithmetic in
computeCalculation()(lines 64–73) produces defensible formulas, not magic numbers. - The draft paragraph output combines original achievement text, calculation formula, and appropriate disclaimers for immediate use in negotiation preparation.
Frequently Asked Questions
What happens if a number appears in my story bank but not in my CV?
The claim is marked uncalculable with reason UNVERIFIED. The verifyClaim() function requires verbatim presence of both the "before" and "after" values (with units) in cv.md. This prevents unverified achievements from entering salary discussions where they could be challenged.
Can I override the wage or frequency if my story doesn't specify them?
Yes. Use CLI flags --wage and --frequency (or --occurrences). The resolveWage() and resolveFrequency() functions check explicit flags only when story text lacks the required data. If neither source provides the value, the claim remains uncalculable rather than using a default.
Why does the script use boundary-aware regex for verification?
To eliminate false positives. Without word boundaries, "8 hours" could match "18 hours", "80 hours", or "8.5 hours" in cv.md. The regex pattern (?<![\\d.])8 hours(?![\\d.]) requires non-digit, non-decimal characters on both sides, ensuring exact matches only.
What output formats does negotiation-roi.mjs support?
The CLI defaults to human-readable summary with --summary, or structured JSON without flags. The programmatic analyze() function returns a JavaScript object containing calculable talking points, uncalculable items with reasons, warnings, and scanning statistics—suitable for further automation or integration into document generators.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →