job-forge 2.14.16 → 2.14.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/main.mdc +4 -1
- package/.opencode/skills/job-forge.md +4 -0
- package/AGENTS.md +4 -1
- package/CLAUDE.md +4 -1
- package/README.md +3 -2
- package/bin/job-forge.mjs +1 -1
- package/docs/ARCHITECTURE.md +8 -7
- package/docs/CUSTOMIZATION.md +4 -0
- package/docs/SETUP.md +1 -0
- package/iso/commands/job-forge.md +4 -0
- package/iso/instructions.md +4 -1
- package/lib/jobforge-contracts.mjs +109 -0
- package/merge-tracker.mjs +26 -55
- package/modes/reference-setup.md +17 -4
- package/package.json +2 -1
- package/scripts/tracker-line.mjs +10 -4
- package/templates/contracts.json +54 -0
- package/verify-pipeline.mjs +56 -7
package/.cursor/rules/main.mdc
CHANGED
|
@@ -62,6 +62,9 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
62
62
|
- [D8] Use `job-forge ledger:*` for cheap local workflow-state checks when available. `iso-ledger` is not an MCP and adds no prompt/tool schema tokens; it records tracker TSV writes, merge outcomes, rebuilt tracker snapshots, and pipeline items in `.jobforge-ledger/events.jsonl`.
|
|
63
63
|
why: state-trace remains working memory, while iso-ledger is deterministic append-only workflow truth that can answer duplicate/status questions without loading growing markdown/TSV files into the model context
|
|
64
64
|
|
|
65
|
+
- [D9] Treat `templates/contracts.json` as the source of truth for machine-readable artifacts. Prefer `npx job-forge tracker-line ... --write` for tracker additions; if emitting TSV manually, inspect `npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json` first. `merge` and `verify` enforce the tracker-row contract locally.
|
|
66
|
+
why: deterministic code owns the exact tracker TSV/table shape; repeated prose gets re-tokenized and agents occasionally misremember it
|
|
67
|
+
|
|
65
68
|
## Procedure
|
|
66
69
|
|
|
67
70
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
@@ -72,7 +75,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
72
75
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
|
73
76
|
7. Cross-check subagent facts against authoritative files [H7].
|
|
74
77
|
8. Apply score gate [D4].
|
|
75
|
-
9. Merge TSV outcomes [H6].
|
|
78
|
+
9. Merge contract-validated TSV outcomes [H6, D9].
|
|
76
79
|
10. Verify tracker before ending [H6].
|
|
77
80
|
|
|
78
81
|
## Routing
|
|
@@ -72,6 +72,10 @@ Token usage check (terminal, outside opencode):
|
|
|
72
72
|
Local workflow ledger (terminal, outside opencode):
|
|
73
73
|
npx job-forge ledger:status # .jobforge-ledger/events.jsonl summary
|
|
74
74
|
npx job-forge ledger:has --company "Acme" --role "Staff Engineer" --status Applied
|
|
75
|
+
|
|
76
|
+
Artifact contracts (terminal, outside opencode):
|
|
77
|
+
npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json
|
|
78
|
+
npx job-forge tracker-line ... --write # renders + validates tracker TSV locally
|
|
75
79
|
```
|
|
76
80
|
|
|
77
81
|
---
|
package/AGENTS.md
CHANGED
|
@@ -57,6 +57,9 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
57
57
|
- [D8] Use `job-forge ledger:*` for cheap local workflow-state checks when available. `iso-ledger` is not an MCP and adds no prompt/tool schema tokens; it records tracker TSV writes, merge outcomes, rebuilt tracker snapshots, and pipeline items in `.jobforge-ledger/events.jsonl`.
|
|
58
58
|
why: state-trace remains working memory, while iso-ledger is deterministic append-only workflow truth that can answer duplicate/status questions without loading growing markdown/TSV files into the model context
|
|
59
59
|
|
|
60
|
+
- [D9] Treat `templates/contracts.json` as the source of truth for machine-readable artifacts. Prefer `npx job-forge tracker-line ... --write` for tracker additions; if emitting TSV manually, inspect `npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json` first. `merge` and `verify` enforce the tracker-row contract locally.
|
|
61
|
+
why: deterministic code owns the exact tracker TSV/table shape; repeated prose gets re-tokenized and agents occasionally misremember it
|
|
62
|
+
|
|
60
63
|
## Procedure
|
|
61
64
|
|
|
62
65
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
@@ -67,7 +70,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
67
70
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
|
68
71
|
7. Cross-check subagent facts against authoritative files [H7].
|
|
69
72
|
8. Apply score gate [D4].
|
|
70
|
-
9. Merge TSV outcomes [H6].
|
|
73
|
+
9. Merge contract-validated TSV outcomes [H6, D9].
|
|
71
74
|
10. Verify tracker before ending [H6].
|
|
72
75
|
|
|
73
76
|
## Routing
|
package/CLAUDE.md
CHANGED
|
@@ -57,6 +57,9 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
57
57
|
- [D8] Use `job-forge ledger:*` for cheap local workflow-state checks when available. `iso-ledger` is not an MCP and adds no prompt/tool schema tokens; it records tracker TSV writes, merge outcomes, rebuilt tracker snapshots, and pipeline items in `.jobforge-ledger/events.jsonl`.
|
|
58
58
|
why: state-trace remains working memory, while iso-ledger is deterministic append-only workflow truth that can answer duplicate/status questions without loading growing markdown/TSV files into the model context
|
|
59
59
|
|
|
60
|
+
- [D9] Treat `templates/contracts.json` as the source of truth for machine-readable artifacts. Prefer `npx job-forge tracker-line ... --write` for tracker additions; if emitting TSV manually, inspect `npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json` first. `merge` and `verify` enforce the tracker-row contract locally.
|
|
61
|
+
why: deterministic code owns the exact tracker TSV/table shape; repeated prose gets re-tokenized and agents occasionally misremember it
|
|
62
|
+
|
|
60
63
|
## Procedure
|
|
61
64
|
|
|
62
65
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
@@ -67,7 +70,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
67
70
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
|
68
71
|
7. Cross-check subagent facts against authoritative files [H7].
|
|
69
72
|
8. Apply score gate [D4].
|
|
70
|
-
9. Merge TSV outcomes [H6].
|
|
73
|
+
9. Merge contract-validated TSV outcomes [H6, D9].
|
|
71
74
|
10. Verify tracker before ending [H6].
|
|
72
75
|
|
|
73
76
|
## Routing
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ The scaffolded `opencode.json` already has three MCPs wired up — they launch a
|
|
|
31
31
|
- **Gmail** — reads replies from recruiters
|
|
32
32
|
- **state-trace** — typed working memory for cross-session context (resumed batches, recent decisions, repeated portal quirks). Install once with `python3 -m pip install "state-trace[mcp]"`; the MCP command is `state-trace-mcp`.
|
|
33
33
|
|
|
34
|
-
JobForge also keeps
|
|
34
|
+
JobForge also keeps MCP-free local workflow state: `templates/contracts.json` defines tracker/apply artifact shapes via `@razroo/iso-contract`, and `.jobforge-ledger/events.jsonl` records deterministic duplicate/status events via `@razroo/iso-ledger`. Neither adds prompt or tool-schema tokens.
|
|
35
35
|
|
|
36
36
|
`npm install` also materializes symlinks for every supported agent harness — OpenCode, Cursor, Claude Code, and Codex — so you can run `opencode`, `cursor`, `claude`, or `codex` in the same project and each picks up the shared MCP config and instructions.
|
|
37
37
|
|
|
@@ -78,7 +78,7 @@ JobForge turns opencode into a full job search command center. Instead of manual
|
|
|
78
78
|
| **Durable Batch Orchestration** | `batch-runner.sh` uses `@razroo/iso-orchestrator` for resumable bundle execution, bounded fan-out, mutexed state writes, and workflow records in `.jobforge-runs/`. |
|
|
79
79
|
| **Pipeline Integrity** | Automated merge, dedup, status normalization, health checks |
|
|
80
80
|
| **Cost-Aware Agent Routing** | Three subagents (`@general-free`, `@general-paid`, `@glm-minimal`) with per-task tool surfaces. On OpenCode, JobForge pins all tiers to `opencode-go/deepseek-v4-flash` so application runs avoid overloaded free-model pools. See [Subagent Routing in AGENTS.md](AGENTS.md) for the task-to-agent mapping. |
|
|
81
|
-
| **Trace + Telemetry + Guard + Ledger** | `job-forge trace:*` exposes local OpenCode transcripts, `job-forge telemetry:*` summarizes runs, `job-forge guard:*` audits deterministic policy rules, and `job-forge ledger:*` queries append-only workflow state without MCP/token overhead. |
|
|
81
|
+
| **Trace + Telemetry + Guard + Contract + Ledger** | `job-forge trace:*` exposes local OpenCode transcripts, `job-forge telemetry:*` summarizes runs, `job-forge guard:*` audits deterministic policy rules, `templates/contracts.json` enforces artifact shape with `iso-contract`, and `job-forge ledger:*` queries append-only workflow state without MCP/token overhead. |
|
|
82
82
|
| **Token Cost Visibility** | `job-forge tokens --days 1` for per-session breakdown; `job-forge session-report --since-minutes 60 --log` to flag sessions over budget and append history to `data/token-usage.tsv`. Auto-logged after every batch run. |
|
|
83
83
|
|
|
84
84
|
## Usage
|
|
@@ -193,6 +193,7 @@ JobForge/
|
|
|
193
193
|
├── batch/{batch-prompt.md,batch-runner.sh} # batch orchestrator
|
|
194
194
|
├── scripts/
|
|
195
195
|
│ ├── batch-orchestrator.mjs # iso-orchestrator-backed batch control loop
|
|
196
|
+
│ ├── tracker-line.mjs # iso-contract-backed tracker TSV renderer
|
|
196
197
|
│ ├── ledger.mjs # iso-ledger-backed workflow-state CLI
|
|
197
198
|
│ ├── token-usage-report.mjs # opencode cost analyzer
|
|
198
199
|
│ └── release/check-source.mjs # version gate for npm publish
|
package/bin/job-forge.mjs
CHANGED
|
@@ -120,7 +120,7 @@ Deterministic helpers (prefer these over LLM-derived values):
|
|
|
120
120
|
next-num Print next sequential report number (e.g. 521)
|
|
121
121
|
slugify NAME Convert a company/role name to a filename-safe slug
|
|
122
122
|
today Print today's date in YYYY-MM-DD
|
|
123
|
-
tracker-line
|
|
123
|
+
tracker-line Render and validate a tracker TSV row for batch/tracker-additions/
|
|
124
124
|
|
|
125
125
|
Cost visibility:
|
|
126
126
|
session-report Summarize recent session costs, warn on >budget sessions
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -173,12 +173,12 @@ Create `data/pipeline.md` when you start using the URL inbox (`/job-forge pipeli
|
|
|
173
173
|
|
|
174
174
|
- Reports: `{###}-{company-slug}-{YYYY-MM-DD}.md` (3-digit zero-padded)
|
|
175
175
|
- PDFs: `cv-candidate-{company-slug}-{YYYY-MM-DD}.pdf`
|
|
176
|
-
- Tracker TSVs: `batch/tracker-additions/{num}-{company-slug}.tsv` (one file per evaluation; merged files move under `batch/tracker-additions/merged
|
|
176
|
+
- Tracker TSVs: `batch/tracker-additions/{num}-{company-slug}.tsv` (one file per evaluation; merged files move under `batch/tracker-additions/merged/`; shape enforced by `templates/contracts.json`)
|
|
177
177
|
- Ledger: `.jobforge-ledger/events.jsonl` (created by `job-forge ledger:rebuild`, `tracker-line --write`, or `merge`; gitignored personal state)
|
|
178
178
|
|
|
179
179
|
## Pipeline Integrity
|
|
180
180
|
|
|
181
|
-
From the project root, `npx job-forge verify` (or `npm run verify`) runs `verify-pipeline.mjs`. When a tracker file exists, it validates canonical statuses (using `templates/states.yml` when that file is present and parseable), warns on probable duplicate company/role rows, checks that report column markdown links resolve to files in the repo, validates score column format (`X.X/5`, `N/A`, or `DUP`), rejects table rows with too few columns, flags markdown bold inside the score column, and warns if any `batch/tracker-additions/*.tsv` files are still waiting to be merged. If `.jobforge-ledger/events.jsonl` exists, verify also validates the append-only ledger. It also compares state ids from `templates/states.yml` to an internal fallback list and warns when the two sets drift. **Fresh clone:** the command exits successfully when neither `data/applications.md` nor root `applications.md` exists yet; pending-TSV and states-drift checks still run so contributors see unmerged batch output early. Optional setup validation after you add `cv.md` and `config/profile.yml`: `npm run sync-check` (`cv-sync-check.mjs`).
|
|
181
|
+
From the project root, `npx job-forge verify` (or `npm run verify`) runs `verify-pipeline.mjs`. When a tracker file exists, it validates canonical statuses (using `templates/states.yml` when that file is present and parseable), validates every tracker row against `templates/contracts.json`, warns on probable duplicate company/role rows, checks that report column markdown links resolve to files in the repo, validates score column format (`X.X/5`, `N/A`, or `DUP`), rejects table rows with too few columns, flags markdown bold inside the score column, and warns if any `batch/tracker-additions/*.tsv` files are still waiting to be merged. If `.jobforge-ledger/events.jsonl` exists, verify also validates the append-only ledger. It also compares state ids from `templates/states.yml` to an internal fallback list and warns when the two sets drift. **Fresh clone:** the command exits successfully when neither `data/applications.md` nor root `applications.md` exists yet; pending-TSV and states-drift checks still run so contributors see unmerged batch output early. Optional setup validation after you add `cv.md` and `config/profile.yml`: `npm run sync-check` (`cv-sync-check.mjs`).
|
|
182
182
|
|
|
183
183
|
**`verify-pipeline.mjs` checks (same order as the script header):**
|
|
184
184
|
|
|
@@ -187,12 +187,13 @@ From the project root, `npx job-forge verify` (or `npm run verify`) runs `verify
|
|
|
187
187
|
3. Report column markdown links resolve to files under the repo root.
|
|
188
188
|
4. Score column matches `X.X/5`, `N/A`, or `DUP`.
|
|
189
189
|
5. Table data rows have enough pipe-delimited columns.
|
|
190
|
-
6.
|
|
191
|
-
7.
|
|
192
|
-
8.
|
|
193
|
-
9.
|
|
190
|
+
6. Tracker rows satisfy the `jobforge.tracker-row` contract in `templates/contracts.json`.
|
|
191
|
+
7. No unmerged `batch/tracker-additions/*.tsv` files (warns if any remain).
|
|
192
|
+
8. Score column has no markdown bold.
|
|
193
|
+
9. Warn when state ids in `templates/states.yml` drift from the script’s built-in fallback list (or when the file exists but ids failed to parse).
|
|
194
|
+
10. Validate `.jobforge-ledger/events.jsonl` when present.
|
|
194
195
|
|
|
195
|
-
When the tracker file is missing, checks 1
|
|
196
|
+
When the tracker file is missing, checks 1-6 and 8 are skipped; checks 7, 9, and 10 still run when applicable.
|
|
196
197
|
|
|
197
198
|
## Contributing touchpoints
|
|
198
199
|
|
package/docs/CUSTOMIZATION.md
CHANGED
|
@@ -138,6 +138,10 @@ npx job-forge ledger:verify
|
|
|
138
138
|
|
|
139
139
|
`tracker-line --write` records tracker-addition events, `merge` records add/update/skip outcomes, and `ledger:rebuild` backfills events from `data/applications/`, `batch/tracker-additions/`, `batch/tracker-additions/merged/`, and `data/pipeline.md`.
|
|
140
140
|
|
|
141
|
+
## JobForge artifact contracts
|
|
142
|
+
|
|
143
|
+
Machine-readable artifact shapes live in `templates/contracts.json` and are enforced by `@razroo/iso-contract`. `job-forge tracker-line` renders tracker additions through the `jobforge.tracker-row` contract, `merge` validates pending TSV/table rows before writing tracker files, and `verify` validates existing tracker rows against the same contract. Custom forks can extend `templates/contracts.json`, but keep the tracker status enum aligned with `templates/states.yml`.
|
|
144
|
+
|
|
141
145
|
## JobForge guard audits
|
|
142
146
|
|
|
143
147
|
Guard audits run deterministic `@razroo/iso-guard` policies over the same local OpenCode traces. The default policy lives at `templates/guards/jobforge-baseline.yaml` and checks rules that are reliable from transcript data, including max two task dispatches per assistant message, no task-status polling via `task`, no raw proxy configuration in task prompts, and no child session task recursion.
|
package/docs/SETUP.md
CHANGED
|
@@ -125,6 +125,7 @@ From your project root, these commands maintain the tracker and pipeline checks.
|
|
|
125
125
|
|--------|---------|-----------|
|
|
126
126
|
| Pipeline health check | `npx job-forge verify` | `npm run verify` |
|
|
127
127
|
| Merge `batch/tracker-additions/*.tsv` into the tracker | `npx job-forge merge` | `npm run merge` |
|
|
128
|
+
| Inspect tracker row contract | `npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json` | _(none)_ |
|
|
128
129
|
| Map status column to canonical labels | `npx job-forge normalize` | `npm run normalize` |
|
|
129
130
|
| Merge duplicate company/role rows | `npx job-forge dedup` | `npm run dedup` |
|
|
130
131
|
| Generate ATS-optimized CV PDF | `npx job-forge pdf` | `npm run pdf` |
|
|
@@ -75,6 +75,10 @@ Token usage check (terminal, outside opencode):
|
|
|
75
75
|
Local workflow ledger (terminal, outside opencode):
|
|
76
76
|
npx job-forge ledger:status # .jobforge-ledger/events.jsonl summary
|
|
77
77
|
npx job-forge ledger:has --company "Acme" --role "Staff Engineer" --status Applied
|
|
78
|
+
|
|
79
|
+
Artifact contracts (terminal, outside opencode):
|
|
80
|
+
npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json
|
|
81
|
+
npx job-forge tracker-line ... --write # renders + validates tracker TSV locally
|
|
78
82
|
```
|
|
79
83
|
|
|
80
84
|
---
|
package/iso/instructions.md
CHANGED
|
@@ -57,6 +57,9 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
57
57
|
- [D8] Use `job-forge ledger:*` for cheap local workflow-state checks when available. `iso-ledger` is not an MCP and adds no prompt/tool schema tokens; it records tracker TSV writes, merge outcomes, rebuilt tracker snapshots, and pipeline items in `.jobforge-ledger/events.jsonl`.
|
|
58
58
|
why: state-trace remains working memory, while iso-ledger is deterministic append-only workflow truth that can answer duplicate/status questions without loading growing markdown/TSV files into the model context
|
|
59
59
|
|
|
60
|
+
- [D9] Treat `templates/contracts.json` as the source of truth for machine-readable artifacts. Prefer `npx job-forge tracker-line ... --write` for tracker additions; if emitting TSV manually, inspect `npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json` first. `merge` and `verify` enforce the tracker-row contract locally.
|
|
61
|
+
why: deterministic code owns the exact tracker TSV/table shape; repeated prose gets re-tokenized and agents occasionally misremember it
|
|
62
|
+
|
|
60
63
|
## Procedure
|
|
61
64
|
|
|
62
65
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
@@ -67,7 +70,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
67
70
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
|
68
71
|
7. Cross-check subagent facts against authoritative files [H7].
|
|
69
72
|
8. Apply score gate [D4].
|
|
70
|
-
9. Merge TSV outcomes [H6].
|
|
73
|
+
9. Merge contract-validated TSV outcomes [H6, D9].
|
|
71
74
|
10. Verify tracker before ending [H6].
|
|
72
75
|
|
|
73
76
|
## Routing
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { dirname, join, resolve } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import {
|
|
5
|
+
contractNames,
|
|
6
|
+
explainContract,
|
|
7
|
+
formatIssue,
|
|
8
|
+
getContract,
|
|
9
|
+
loadContractCatalog,
|
|
10
|
+
parseRecord,
|
|
11
|
+
renderRecord,
|
|
12
|
+
validateRecord,
|
|
13
|
+
} from '@razroo/iso-contract';
|
|
14
|
+
import { DEFAULT_STATES, loadCanonicalStates } from './canonical-states.mjs';
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const PKG_ROOT = resolve(__dirname, '..');
|
|
18
|
+
export const CONTRACTS_RELATIVE_PATH = 'templates/contracts.json';
|
|
19
|
+
|
|
20
|
+
export function resolveProjectDir(projectDir = process.env.JOB_FORGE_PROJECT || process.cwd()) {
|
|
21
|
+
return projectDir;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function jobForgeContractsPath(projectDir = resolveProjectDir()) {
|
|
25
|
+
const projectPath = join(projectDir, CONTRACTS_RELATIVE_PATH);
|
|
26
|
+
if (existsSync(projectPath)) return projectPath;
|
|
27
|
+
return join(PKG_ROOT, CONTRACTS_RELATIVE_PATH);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function loadJobForgeContractCatalog(projectDir = resolveProjectDir()) {
|
|
31
|
+
const path = jobForgeContractsPath(projectDir);
|
|
32
|
+
const input = JSON.parse(readFileSync(path, 'utf-8'));
|
|
33
|
+
applyCanonicalStatusValues(input, projectDir);
|
|
34
|
+
return loadContractCatalog(input);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function listJobForgeContracts(projectDir = resolveProjectDir()) {
|
|
38
|
+
return contractNames(loadJobForgeContractCatalog(projectDir));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getJobForgeContract(name, projectDir = resolveProjectDir()) {
|
|
42
|
+
return getContract(loadJobForgeContractCatalog(projectDir), name);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function explainJobForgeContract(name, projectDir = resolveProjectDir()) {
|
|
46
|
+
return explainContract(getJobForgeContract(name, projectDir));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getTrackerRowContract(projectDir = resolveProjectDir()) {
|
|
50
|
+
return getJobForgeContract('jobforge.tracker-row', projectDir);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function validateTrackerRow(record, options = {}) {
|
|
54
|
+
const projectDir = resolveProjectDir(options.projectDir);
|
|
55
|
+
const normalized = normalizeTrackerRowRecord(record, options);
|
|
56
|
+
const contract = options.allowMissingReport
|
|
57
|
+
? trackerRowContractWithOptionalReport(getTrackerRowContract(projectDir))
|
|
58
|
+
: getTrackerRowContract(projectDir);
|
|
59
|
+
return validateRecord(contract, normalized);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function parseTrackerRow(text, formatName = 'tsv', options = {}) {
|
|
63
|
+
const projectDir = resolveProjectDir(options.projectDir);
|
|
64
|
+
const contract = getTrackerRowContract(projectDir);
|
|
65
|
+
const parsed = parseRecord(contract, text, formatName);
|
|
66
|
+
const normalized = normalizeTrackerRowRecord(parsed.record, options);
|
|
67
|
+
const validation = validateRecord(contract, normalized);
|
|
68
|
+
return { record: validation.record, validation, format: formatName };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function renderTrackerRow(record, formatName = 'tsv', options = {}) {
|
|
72
|
+
const projectDir = resolveProjectDir(options.projectDir);
|
|
73
|
+
const normalized = normalizeTrackerRowRecord(record, options);
|
|
74
|
+
return renderRecord(getTrackerRowContract(projectDir), normalized, formatName);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function formatContractIssues(result) {
|
|
78
|
+
return result.issues.map(formatIssue).join('; ');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function canonicalStatusValues(projectDir = resolveProjectDir()) {
|
|
82
|
+
return loadCanonicalStates(projectDir) || loadCanonicalStates(PKG_ROOT) || DEFAULT_STATES;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeTrackerRowRecord(record, options = {}) {
|
|
86
|
+
const out = { ...record };
|
|
87
|
+
if (out.status !== undefined && options.normalizeStatus) {
|
|
88
|
+
out.status = options.normalizeStatus(String(out.status));
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function trackerRowContractWithOptionalReport(contract) {
|
|
94
|
+
return {
|
|
95
|
+
...contract,
|
|
96
|
+
fields: contract.fields.map((field) => (
|
|
97
|
+
field.name === 'report' ? { ...field, required: false } : field
|
|
98
|
+
)),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function applyCanonicalStatusValues(input, projectDir) {
|
|
103
|
+
const statuses = canonicalStatusValues(projectDir);
|
|
104
|
+
for (const contract of input.contracts || []) {
|
|
105
|
+
if (contract.name !== 'jobforge.tracker-row') continue;
|
|
106
|
+
const field = (contract.fields || []).find((item) => item.name === 'status');
|
|
107
|
+
if (field) field.values = statuses;
|
|
108
|
+
}
|
|
109
|
+
}
|
package/merge-tracker.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
DEFAULT_STATES, loadCanonicalStates, buildStatusDetectionRegex,
|
|
31
31
|
} from './lib/canonical-states.mjs';
|
|
32
32
|
import { recordTrackerMergeResult } from './lib/jobforge-ledger.mjs';
|
|
33
|
+
import { formatContractIssues, parseTrackerRow } from './lib/jobforge-contracts.mjs';
|
|
33
34
|
|
|
34
35
|
const ADDITIONS_DIR = join(PROJECT_DIR, 'batch/tracker-additions');
|
|
35
36
|
const MERGED_DIR = join(ADDITIONS_DIR, 'merged');
|
|
@@ -156,64 +157,22 @@ function parseTsvContent(content, filename) {
|
|
|
156
157
|
content = content.trim();
|
|
157
158
|
if (!content) return null;
|
|
158
159
|
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
const format = detectTrackerRowFormat(content);
|
|
161
|
+
const parsed = parseTrackerRow(content, format, {
|
|
162
|
+
projectDir: PROJECT_DIR,
|
|
163
|
+
normalizeStatus: validateStatus,
|
|
164
|
+
});
|
|
161
165
|
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
console.warn(`⚠️ Skipping malformed pipe-delimited ${filename}: ${parts.length} fields`);
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
addition = {
|
|
169
|
-
num: parseInt(parts[0]),
|
|
170
|
-
date: parts[1],
|
|
171
|
-
company: parts[2],
|
|
172
|
-
role: parts[3],
|
|
173
|
-
score: parts[4],
|
|
174
|
-
status: validateStatus(parts[5]),
|
|
175
|
-
pdf: parts[6],
|
|
176
|
-
report: parts[7],
|
|
177
|
-
notes: parts[8] || '',
|
|
178
|
-
};
|
|
179
|
-
} else {
|
|
180
|
-
parts = content.split('\t');
|
|
181
|
-
if (parts.length < 8) {
|
|
182
|
-
console.warn(`⚠️ Skipping malformed TSV ${filename}: ${parts.length} fields`);
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const col4 = parts[4].trim();
|
|
187
|
-
const col5 = parts[5].trim();
|
|
188
|
-
const col4LooksLikeScore = /^\d+\.?\d*\/5$/.test(col4) || col4 === 'N/A' || col4 === 'DUP';
|
|
189
|
-
const col5LooksLikeScore = /^\d+\.?\d*\/5$/.test(col5) || col5 === 'N/A' || col5 === 'DUP';
|
|
190
|
-
const col4LooksLikeStatus = STATUS_DETECT_RE.test(col4);
|
|
191
|
-
const col5LooksLikeStatus = STATUS_DETECT_RE.test(col5);
|
|
192
|
-
|
|
193
|
-
let statusCol, scoreCol;
|
|
194
|
-
if (col4LooksLikeStatus && !col4LooksLikeScore) {
|
|
195
|
-
statusCol = col4; scoreCol = col5;
|
|
196
|
-
} else if (col4LooksLikeScore && col5LooksLikeStatus) {
|
|
197
|
-
statusCol = col5; scoreCol = col4;
|
|
198
|
-
} else if (col5LooksLikeScore && !col4LooksLikeScore) {
|
|
199
|
-
statusCol = col4; scoreCol = col5;
|
|
200
|
-
} else {
|
|
201
|
-
statusCol = col4; scoreCol = col5;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
addition = {
|
|
205
|
-
num: parseInt(parts[0]),
|
|
206
|
-
date: parts[1],
|
|
207
|
-
company: parts[2],
|
|
208
|
-
role: parts[3],
|
|
209
|
-
status: validateStatus(statusCol),
|
|
210
|
-
score: scoreCol,
|
|
211
|
-
pdf: parts[6],
|
|
212
|
-
report: parts[7],
|
|
213
|
-
notes: parts[8] || '',
|
|
214
|
-
};
|
|
166
|
+
if (!parsed.validation.ok) {
|
|
167
|
+
console.warn(`⚠️ Skipping ${filename}: tracker contract failed (${format}) — ${formatContractIssues(parsed.validation)}`);
|
|
168
|
+
return null;
|
|
215
169
|
}
|
|
216
170
|
|
|
171
|
+
const addition = {
|
|
172
|
+
...parsed.validation.record,
|
|
173
|
+
num: Number(parsed.validation.record.num),
|
|
174
|
+
};
|
|
175
|
+
|
|
217
176
|
if (isNaN(addition.num) || addition.num === 0) {
|
|
218
177
|
console.warn(`⚠️ Skipping ${filename}: invalid entry number`);
|
|
219
178
|
return null;
|
|
@@ -222,6 +181,18 @@ function parseTsvContent(content, filename) {
|
|
|
222
181
|
return addition;
|
|
223
182
|
}
|
|
224
183
|
|
|
184
|
+
function detectTrackerRowFormat(content) {
|
|
185
|
+
if (content.startsWith('|')) return 'markdown';
|
|
186
|
+
|
|
187
|
+
const parts = content.split('\t');
|
|
188
|
+
const col4 = (parts[4] || '').trim();
|
|
189
|
+
const col5 = (parts[5] || '').trim();
|
|
190
|
+
const col4LooksLikeScore = /^\d+\.?\d*\/5$/.test(col4) || col4 === 'N/A' || col4 === 'DUP';
|
|
191
|
+
const col5LooksLikeStatus = STATUS_DETECT_RE.test(col5);
|
|
192
|
+
|
|
193
|
+
return col4LooksLikeScore && col5LooksLikeStatus ? 'day-tsv' : 'tsv';
|
|
194
|
+
}
|
|
195
|
+
|
|
225
196
|
// ---- Main ----
|
|
226
197
|
|
|
227
198
|
if (!existsSync(ADDITIONS_DIR)) {
|
package/modes/reference-setup.md
CHANGED
|
@@ -112,6 +112,18 @@ Mode routing is specified in the top-level **## Routing** section. Each mode is
|
|
|
112
112
|
|
|
113
113
|
## TSV Format for Tracker Additions
|
|
114
114
|
|
|
115
|
+
Prefer the deterministic helper:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npx job-forge tracker-line --num 521 --date 2026-04-15 --company "Anthropic" --role "Manager, FDE" --status Evaluated --score 4.2 --pdf no --slug anthropic-manager-fde --notes "Strong fit" --write
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The helper renders and validates the row against `templates/contracts.json` via `@razroo/iso-contract`. To inspect the contract directly:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json
|
|
125
|
+
```
|
|
126
|
+
|
|
115
127
|
Write one TSV file per evaluation to `batch/tracker-additions/{num}-{company-slug}.tsv`. Single line, 9 tab-separated columns:
|
|
116
128
|
|
|
117
129
|
```
|
|
@@ -129,7 +141,7 @@ Write one TSV file per evaluation to `batch/tracker-additions/{num}-{company-slu
|
|
|
129
141
|
8. `report` -- markdown link `[num](reports/...)`
|
|
130
142
|
9. `notes` -- one-line summary
|
|
131
143
|
|
|
132
|
-
**Note:** In applications.md, score comes BEFORE status. The merge script handles this column swap automatically.
|
|
144
|
+
**Note:** In applications.md, score comes BEFORE status. The merge script handles this column swap automatically and validates both shapes with the tracker-row contract.
|
|
133
145
|
|
|
134
146
|
- Scripts in `.mjs`, configuration in YAML
|
|
135
147
|
- Output in `output/` (gitignored), Reports in `reports/`
|
|
@@ -146,9 +158,10 @@ Write one TSV file per evaluation to `batch/tracker-additions/{num}-{company-slu
|
|
|
146
158
|
2. **YES you can edit day files in `data/applications/` to UPDATE status/notes of existing entries.**
|
|
147
159
|
3. All reports MUST include `**URL:**` in the header (between Score and PDF).
|
|
148
160
|
4. All statuses MUST be canonical (see `templates/states.yml`).
|
|
149
|
-
5.
|
|
150
|
-
6.
|
|
151
|
-
7.
|
|
161
|
+
5. Tracker rows MUST satisfy `templates/contracts.json`.
|
|
162
|
+
6. Health check: `npx job-forge verify`
|
|
163
|
+
7. Normalize statuses: `npx job-forge normalize`
|
|
164
|
+
8. Dedup: `npx job-forge dedup`
|
|
152
165
|
|
|
153
166
|
### Canonical States (applications day files)
|
|
154
167
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-forge",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.18",
|
|
4
4
|
"description": "AI-powered job search pipeline built on opencode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -96,6 +96,7 @@
|
|
|
96
96
|
"node": ">=20.6.0"
|
|
97
97
|
},
|
|
98
98
|
"dependencies": {
|
|
99
|
+
"@razroo/iso-contract": "^0.1.0",
|
|
99
100
|
"@razroo/iso-guard": "^0.1.0",
|
|
100
101
|
"@razroo/iso-ledger": "^0.1.0",
|
|
101
102
|
"@razroo/iso-orchestrator": "^0.1.0",
|
package/scripts/tracker-line.mjs
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
26
26
|
import { join } from 'path';
|
|
27
27
|
import { recordTrackerAdditionWritten } from '../lib/jobforge-ledger.mjs';
|
|
28
|
+
import { formatContractIssues, renderTrackerRow } from '../lib/jobforge-contracts.mjs';
|
|
28
29
|
|
|
29
30
|
const PROJECT_DIR = process.env.JOB_FORGE_PROJECT || process.cwd();
|
|
30
31
|
|
|
@@ -54,8 +55,15 @@ const write = process.argv.includes('--write');
|
|
|
54
55
|
const paddedNum = String(num).padStart(3, '0');
|
|
55
56
|
const reportLink = `[${num}](reports/${paddedNum}-${slug}-${date}.md)`;
|
|
56
57
|
const scoreField = score.includes('/') ? score : `${score}/5`;
|
|
58
|
+
const record = { num, date, company, role, status, score: scoreField, pdf, report: reportLink, notes };
|
|
59
|
+
const rendered = renderTrackerRow(record, 'tsv', { projectDir: PROJECT_DIR });
|
|
57
60
|
|
|
58
|
-
|
|
61
|
+
if (!rendered.validation.ok) {
|
|
62
|
+
console.error(`tracker-line contract failed: ${formatContractIssues(rendered.validation)}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const line = rendered.text;
|
|
59
67
|
|
|
60
68
|
if (write) {
|
|
61
69
|
const dir = join(PROJECT_DIR, 'batch/tracker-additions');
|
|
@@ -63,9 +71,7 @@ if (write) {
|
|
|
63
71
|
const path = join(dir, `${num}.tsv`);
|
|
64
72
|
writeFileSync(path, line + '\n', 'utf-8');
|
|
65
73
|
try {
|
|
66
|
-
recordTrackerAdditionWritten({
|
|
67
|
-
num, date, company, role, status, score: scoreField, pdf, report: reportLink, notes,
|
|
68
|
-
}, { projectDir: PROJECT_DIR, sourceFile: path });
|
|
74
|
+
recordTrackerAdditionWritten(rendered.validation.record, { projectDir: PROJECT_DIR, sourceFile: path });
|
|
69
75
|
} catch (error) {
|
|
70
76
|
console.warn(`warning: could not append tracker-line ledger event: ${error instanceof Error ? error.message : String(error)}`);
|
|
71
77
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contracts": [
|
|
3
|
+
{
|
|
4
|
+
"name": "jobforge.tracker-row",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"description": "Canonical JobForge tracker row used by batch/tracker-additions and data/applications day files.",
|
|
7
|
+
"fields": [
|
|
8
|
+
{ "name": "num", "type": "integer", "required": true, "min": 1 },
|
|
9
|
+
{ "name": "date", "type": "date", "required": true },
|
|
10
|
+
{ "name": "company", "type": "string", "required": true },
|
|
11
|
+
{ "name": "role", "type": "string", "required": true },
|
|
12
|
+
{
|
|
13
|
+
"name": "status",
|
|
14
|
+
"type": "enum",
|
|
15
|
+
"required": true,
|
|
16
|
+
"values": ["Evaluated", "Applied", "Responded", "Contacted", "Interview", "Offer", "Rejected", "Discarded", "Failed", "SKIP"]
|
|
17
|
+
},
|
|
18
|
+
{ "name": "score", "type": "score", "required": true },
|
|
19
|
+
{ "name": "pdf", "type": "string", "required": true },
|
|
20
|
+
{ "name": "report", "type": "markdown-link", "required": true },
|
|
21
|
+
{ "name": "notes", "type": "string" }
|
|
22
|
+
],
|
|
23
|
+
"formats": {
|
|
24
|
+
"tsv": {
|
|
25
|
+
"style": "delimited",
|
|
26
|
+
"delimiter": "tab",
|
|
27
|
+
"fields": ["num", "date", "company", "role", "status", "score", "pdf", "report", "notes"]
|
|
28
|
+
},
|
|
29
|
+
"day-tsv": {
|
|
30
|
+
"style": "delimited",
|
|
31
|
+
"delimiter": "tab",
|
|
32
|
+
"fields": ["num", "date", "company", "role", "score", "status", "pdf", "report", "notes"]
|
|
33
|
+
},
|
|
34
|
+
"markdown": {
|
|
35
|
+
"style": "markdown-table-row",
|
|
36
|
+
"fields": ["num", "date", "company", "role", "score", "status", "pdf", "report", "notes"]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "jobforge.apply-outcome",
|
|
42
|
+
"version": "1.0.0",
|
|
43
|
+
"description": "Structured final outcome emitted by an application subagent.",
|
|
44
|
+
"fields": [
|
|
45
|
+
{ "name": "status", "type": "enum", "required": true, "values": ["APPLIED", "APPLY FAILED", "SKIP", "Discarded"] },
|
|
46
|
+
{ "name": "company", "type": "string", "required": true },
|
|
47
|
+
{ "name": "role", "type": "string", "required": true },
|
|
48
|
+
{ "name": "url", "type": "url", "required": true },
|
|
49
|
+
{ "name": "trackerTsv", "type": "string" },
|
|
50
|
+
{ "name": "reason", "type": "string" }
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
package/verify-pipeline.mjs
CHANGED
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
* 3. All report links point to existing files
|
|
13
13
|
* 4. Scores match format X.XX/5 or N/A or DUP
|
|
14
14
|
* 5. All rows have proper pipe-delimited format
|
|
15
|
-
* 6.
|
|
16
|
-
* 7. No
|
|
17
|
-
* 8.
|
|
18
|
-
* 9.
|
|
15
|
+
* 6. Tracker rows match templates/contracts.json
|
|
16
|
+
* 7. No pending TSVs in tracker-additions/ (runs even when tracker file is missing)
|
|
17
|
+
* 8. No markdown bold in score column
|
|
18
|
+
* 9. Drift warning if states.yml ids differ from the built-in fallback list
|
|
19
|
+
* 10. Ledger file verifies if .jobforge-ledger/events.jsonl exists
|
|
19
20
|
*
|
|
20
21
|
* Run: node verify-pipeline.mjs (from repo root; same as npm run verify)
|
|
21
22
|
*/
|
|
@@ -28,6 +29,11 @@ import {
|
|
|
28
29
|
usesDayFiles, readAllEntries, listDayFiles, dayFilePath,
|
|
29
30
|
} from './tracker-lib.mjs';
|
|
30
31
|
import { jobForgeLedgerPath, ledgerExists, verifyJobForgeLedger } from './lib/jobforge-ledger.mjs';
|
|
32
|
+
import {
|
|
33
|
+
canonicalStatusValues,
|
|
34
|
+
formatContractIssues,
|
|
35
|
+
validateTrackerRow,
|
|
36
|
+
} from './lib/jobforge-contracts.mjs';
|
|
31
37
|
|
|
32
38
|
const ADDITIONS_DIR = join(PROJECT_DIR, 'batch/tracker-additions');
|
|
33
39
|
const STATES_FILE = existsSync(join(PROJECT_DIR, 'templates/states.yml'))
|
|
@@ -42,7 +48,7 @@ const appsDisplay = usesDayFiles()
|
|
|
42
48
|
|
|
43
49
|
const CANONICAL_STATUSES = [
|
|
44
50
|
'evaluated', 'applied', 'contacted', 'responded', 'interview',
|
|
45
|
-
'offer', 'rejected', 'discarded', 'skip',
|
|
51
|
+
'offer', 'rejected', 'discarded', 'failed', 'skip',
|
|
46
52
|
];
|
|
47
53
|
|
|
48
54
|
const ALIASES = {
|
|
@@ -78,6 +84,7 @@ function loadStatesFromYaml(filePath) {
|
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
const statesMeta = loadStatesFromYaml(STATES_FILE);
|
|
87
|
+
const CONTRACT_STATUSES = canonicalStatusValues(PROJECT_DIR);
|
|
81
88
|
|
|
82
89
|
function statusIsAllowed(statusOnlyLower) {
|
|
83
90
|
if (statesMeta) {
|
|
@@ -167,6 +174,7 @@ console.log(`\n📊 Checking ${entries.length} entries from ${source === 'day' ?
|
|
|
167
174
|
|
|
168
175
|
// --- Check 1: Canonical statuses ---
|
|
169
176
|
let badStatuses = 0;
|
|
177
|
+
// --- Check 6: Contract ---
|
|
170
178
|
for (const e of entries) {
|
|
171
179
|
const clean = e.status.replace(/\*\*/g, '').trim().toLowerCase();
|
|
172
180
|
const statusOnly = clean.replace(/\s+\d{4}-\d{2}-\d{2}.*$/, '').trim();
|
|
@@ -231,6 +239,7 @@ if (badScores === 0) ok('All scores valid');
|
|
|
231
239
|
|
|
232
240
|
// --- Check 5: Row format ---
|
|
233
241
|
let badRows = 0;
|
|
242
|
+
let contractFailures = 0;
|
|
234
243
|
// Re-read raw lines for format check
|
|
235
244
|
if (source === 'day') {
|
|
236
245
|
for (const file of listDayFiles()) {
|
|
@@ -260,10 +269,23 @@ if (source === 'day') {
|
|
|
260
269
|
}
|
|
261
270
|
if (badRows === 0) ok('All rows properly formatted');
|
|
262
271
|
|
|
263
|
-
|
|
272
|
+
for (const e of entries) {
|
|
273
|
+
const result = validateTrackerRow(contractRecordForEntry(e), {
|
|
274
|
+
allowMissingReport: true,
|
|
275
|
+
projectDir: PROJECT_DIR,
|
|
276
|
+
normalizeStatus: normalizeStatusForContract,
|
|
277
|
+
});
|
|
278
|
+
if (!result.ok) {
|
|
279
|
+
error(`#${e.num}: Tracker row contract failed: ${formatContractIssues(result)}`);
|
|
280
|
+
contractFailures++;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (contractFailures === 0) ok('All tracker rows match iso-contract');
|
|
284
|
+
|
|
285
|
+
// --- Check 7: Pending TSVs ---
|
|
264
286
|
checkPendingTrackerAdditions();
|
|
265
287
|
|
|
266
|
-
// --- Check
|
|
288
|
+
// --- Check 8: Bold in scores ---
|
|
267
289
|
let boldScores = 0;
|
|
268
290
|
for (const e of entries) {
|
|
269
291
|
if (e.score.includes('**')) {
|
|
@@ -286,3 +308,30 @@ if (errors === 0 && warnings === 0) {
|
|
|
286
308
|
console.log('🔴 Pipeline has errors — fix before proceeding');
|
|
287
309
|
}
|
|
288
310
|
process.exit(errors > 0 ? 1 : 0);
|
|
311
|
+
|
|
312
|
+
function normalizeStatusForContract(status) {
|
|
313
|
+
const clean = status.replace(/\*\*/g, '').replace(/\s+\d{4}-\d{2}-\d{2}.*$/, '').trim();
|
|
314
|
+
const lower = clean.toLowerCase();
|
|
315
|
+
const direct = CONTRACT_STATUSES.find((value) => value.toLowerCase() === lower);
|
|
316
|
+
if (direct) return direct;
|
|
317
|
+
if (ALIASES[lower] === 'applied') return 'Applied';
|
|
318
|
+
return clean;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function contractRecordForEntry(entry) {
|
|
322
|
+
const record = {
|
|
323
|
+
num: entry.num,
|
|
324
|
+
date: entry.date,
|
|
325
|
+
company: entry.company,
|
|
326
|
+
role: entry.role,
|
|
327
|
+
score: entry.score,
|
|
328
|
+
status: entry.status,
|
|
329
|
+
pdf: entry.pdf,
|
|
330
|
+
report: entry.report,
|
|
331
|
+
notes: entry.notes,
|
|
332
|
+
};
|
|
333
|
+
if (!/\]\([^)]+\)/.test(String(record.report || ''))) {
|
|
334
|
+
delete record.report;
|
|
335
|
+
}
|
|
336
|
+
return record;
|
|
337
|
+
}
|