# How `negotiation-roi.mjs` Generates Salary Negotiation Talking Points from Story-Bank Achievements in santifer/career-ops

> Discover how negotiation-roi.mjs generates salary negotiation talking points from story-bank achievements. This Node.js utility verifies your claims against cv.md to calculate ROI and boost your earnings.

- Repository: [Santiago Fernández de Valderrama/career-ops](https://github.com/santifer/career-ops)
- Tags: how-to-guide
- Published: 2026-08-20

---

**`negotiation-roi.mjs` is a pure Node.js utility that transforms quantified STAR achievements from [`interview-prep/story-bank.md`](https://github.com/santifer/career-ops/blob/main/interview-prep/story-bank.md) into dollar-valued negotiation talking points through a rigorous verification pipeline that cross-checks every claim against [`cv.md`](https://github.com/santifer/career-ops/blob/main/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`](https://github.com/santifer/career-ops/blob/main/interview-prep/story-bank.md). This extraction happens at lines 22–24:

```js
// 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`](https://github.com/santifer/career-ops/blob/main/cv.md) using a **boundary-aware regex**:

```js
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:

1. **Scan the story text** for `$X/hr` or `$X/hour` patterns
2. **Fall back to `--wage` CLI flag**
3. **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:

1. The original achievement sentence (verbatim from story bank)
2. The estimated value formula showing exact calculation
3. 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 values
- `uncalculableItems`: claims with specific reasons (missing wage, missing frequency, unverified)
- `warnings`: edge cases detected during processing
- `counts`: totals of scanned stories and extracted claims

## Practical Usage Examples

### Extract Claims Programmatically

```js
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

```js
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

```js
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

```bash
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`](https://github.com/santifer/career-ops/blob/main/interview-prep/story-bank.md) | Source repository of quantified STAR achievements |
| [`cv.md`](https://github.com/santifer/career-ops/blob/main/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.mjs`** converts 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 in [`cv.md`](https://github.com/santifer/career-ops/blob/main/cv.md) before 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`](https://github.com/santifer/career-ops/blob/main/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`](https://github.com/santifer/career-ops/blob/main/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.