job-forge 2.14.15 → 2.14.17
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 +10 -4
- package/.opencode/skills/job-forge.md +11 -0
- package/AGENTS.md +10 -4
- package/CLAUDE.md +10 -4
- package/README.md +6 -1
- package/bin/create-job-forge.mjs +9 -1
- package/bin/job-forge.mjs +31 -1
- package/docs/ARCHITECTURE.md +12 -6
- package/docs/CUSTOMIZATION.md +17 -0
- package/docs/README.md +1 -1
- package/docs/SETUP.md +4 -0
- package/iso/commands/job-forge.md +11 -0
- package/iso/instructions.md +10 -4
- package/lib/jobforge-contracts.mjs +97 -0
- package/lib/jobforge-ledger.mjs +214 -0
- package/merge-tracker.mjs +49 -55
- package/modes/reference-setup.md +17 -4
- package/package.json +8 -1
- package/scripts/ledger.mjs +359 -0
- package/scripts/telemetry.mjs +14 -0
- package/scripts/tracker-line.mjs +15 -1
- package/templates/contracts.json +54 -0
- package/verify-pipeline.mjs +57 -6
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { isAbsolute, join, relative } from 'path';
|
|
3
|
+
import {
|
|
4
|
+
appendEvent,
|
|
5
|
+
hasEvent,
|
|
6
|
+
materializeLedger,
|
|
7
|
+
queryEvents,
|
|
8
|
+
readLedger,
|
|
9
|
+
verifyLedger,
|
|
10
|
+
} from '@razroo/iso-ledger';
|
|
11
|
+
|
|
12
|
+
export const LEDGER_DIR = '.jobforge-ledger';
|
|
13
|
+
export const LEDGER_FILE = 'events.jsonl';
|
|
14
|
+
|
|
15
|
+
export function resolveProjectDir(projectDir = process.env.JOB_FORGE_PROJECT || process.cwd()) {
|
|
16
|
+
return projectDir;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function jobForgeLedgerPath(projectDir = resolveProjectDir()) {
|
|
20
|
+
return process.env.JOB_FORGE_LEDGER || join(projectDir, LEDGER_DIR, LEDGER_FILE);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function jobForgeLedgerOptions(projectDir = resolveProjectDir()) {
|
|
24
|
+
return { path: jobForgeLedgerPath(projectDir) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ledgerExists(projectDir = resolveProjectDir()) {
|
|
28
|
+
return existsSync(jobForgeLedgerPath(projectDir));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function readJobForgeLedger(projectDir = resolveProjectDir()) {
|
|
32
|
+
if (!ledgerExists(projectDir)) return [];
|
|
33
|
+
return readLedger(jobForgeLedgerOptions(projectDir));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function verifyJobForgeLedger(projectDir = resolveProjectDir()) {
|
|
37
|
+
return verifyLedger(jobForgeLedgerOptions(projectDir));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function queryJobForgeLedger(options = {}, projectDir = resolveProjectDir()) {
|
|
41
|
+
return queryEvents(readJobForgeLedger(projectDir), options);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function hasJobForgeEvent(options = {}, projectDir = resolveProjectDir()) {
|
|
45
|
+
return hasEvent(readJobForgeLedger(projectDir), options);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function jobForgeLedgerSummary(projectDir = resolveProjectDir()) {
|
|
49
|
+
const events = readJobForgeLedger(projectDir);
|
|
50
|
+
const materialized = materializeLedger(events);
|
|
51
|
+
return {
|
|
52
|
+
path: jobForgeLedgerPath(projectDir),
|
|
53
|
+
exists: ledgerExists(projectDir),
|
|
54
|
+
events: events.length,
|
|
55
|
+
entities: materialized.entityCount,
|
|
56
|
+
latest: events.at(-1) || null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function appendJobForgeEvent(input, projectDir = resolveProjectDir()) {
|
|
61
|
+
return appendEvent(jobForgeLedgerOptions(projectDir), input);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function recordTrackerAdditionWritten(addition, options = {}) {
|
|
65
|
+
const projectDir = resolveProjectDir(options.projectDir);
|
|
66
|
+
return appendJobForgeEvent(buildApplicationEvent('jobforge.tracker_addition.written', addition, {
|
|
67
|
+
projectDir,
|
|
68
|
+
sourceFile: options.sourceFile,
|
|
69
|
+
idempotencyPrefix: 'tracker-addition-written',
|
|
70
|
+
meta: options.meta,
|
|
71
|
+
}), projectDir);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function recordTrackerMergeResult(addition, options = {}) {
|
|
75
|
+
const projectDir = resolveProjectDir(options.projectDir);
|
|
76
|
+
const outcome = options.outcome || 'processed';
|
|
77
|
+
return appendJobForgeEvent(buildApplicationEvent(`jobforge.tracker_merge.${outcome}`, addition, {
|
|
78
|
+
projectDir,
|
|
79
|
+
sourceFile: options.sourceFile,
|
|
80
|
+
idempotencyPrefix: `tracker-merge-${outcome}`,
|
|
81
|
+
data: {
|
|
82
|
+
outcome,
|
|
83
|
+
duplicateNum: jsonValue(options.duplicateNum),
|
|
84
|
+
reason: jsonValue(options.reason),
|
|
85
|
+
},
|
|
86
|
+
meta: options.meta,
|
|
87
|
+
}), projectDir);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function buildApplicationEvent(type, app, options = {}) {
|
|
91
|
+
const projectDir = resolveProjectDir(options.projectDir);
|
|
92
|
+
const key = companyRoleKey(app.company, app.role);
|
|
93
|
+
const sourceFile = options.sourceFile ? relativePath(projectDir, options.sourceFile) : '';
|
|
94
|
+
const idempotencyParts = [
|
|
95
|
+
options.idempotencyPrefix || type,
|
|
96
|
+
sourceFile,
|
|
97
|
+
app.num,
|
|
98
|
+
app.date,
|
|
99
|
+
key,
|
|
100
|
+
app.status,
|
|
101
|
+
app.score,
|
|
102
|
+
].filter((value) => value !== undefined && value !== null && String(value).length > 0);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
type,
|
|
106
|
+
key,
|
|
107
|
+
subject: applicationSubject(app.company, app.role),
|
|
108
|
+
idempotencyKey: idempotencyParts.join(':'),
|
|
109
|
+
data: compactObject({
|
|
110
|
+
num: numberOrString(app.num),
|
|
111
|
+
date: stringOrEmpty(app.date),
|
|
112
|
+
company: stringOrEmpty(app.company),
|
|
113
|
+
role: stringOrEmpty(app.role),
|
|
114
|
+
score: stringOrEmpty(app.score),
|
|
115
|
+
status: stringOrEmpty(app.status),
|
|
116
|
+
pdf: stringOrEmpty(app.pdf),
|
|
117
|
+
report: stringOrEmpty(app.report),
|
|
118
|
+
notes: stringOrEmpty(app.notes),
|
|
119
|
+
sourceFile,
|
|
120
|
+
...compactObject(options.data || {}),
|
|
121
|
+
}),
|
|
122
|
+
meta: compactObject({
|
|
123
|
+
source: 'job-forge',
|
|
124
|
+
...compactObject(options.meta || {}),
|
|
125
|
+
}),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function buildPipelineEvent(item, options = {}) {
|
|
130
|
+
const projectDir = resolveProjectDir(options.projectDir);
|
|
131
|
+
const key = item.url ? urlKey(item.url) : `pipeline:${item.lineNumber || 'unknown'}`;
|
|
132
|
+
const sourceFile = options.sourceFile ? relativePath(projectDir, options.sourceFile) : 'data/pipeline.md';
|
|
133
|
+
const state = item.checked ? 'processed' : 'pending';
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
type: 'jobforge.pipeline.item',
|
|
137
|
+
key,
|
|
138
|
+
subject: key,
|
|
139
|
+
idempotencyKey: `pipeline:${state}:${item.url || item.lineNumber || item.line || 'unknown'}`,
|
|
140
|
+
data: compactObject({
|
|
141
|
+
state,
|
|
142
|
+
checked: Boolean(item.checked),
|
|
143
|
+
url: stringOrEmpty(item.url),
|
|
144
|
+
company: stringOrEmpty(item.company),
|
|
145
|
+
role: stringOrEmpty(item.role),
|
|
146
|
+
line: stringOrEmpty(item.line),
|
|
147
|
+
lineNumber: numberOrString(item.lineNumber),
|
|
148
|
+
sourceFile,
|
|
149
|
+
}),
|
|
150
|
+
meta: compactObject({
|
|
151
|
+
source: 'job-forge',
|
|
152
|
+
...compactObject(options.meta || {}),
|
|
153
|
+
}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function companyRoleKey(company, role) {
|
|
158
|
+
return `company-role:${slugPart(company)}:${slugPart(role)}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function applicationSubject(company, role) {
|
|
162
|
+
return `application:${slugPart(company)}:${slugPart(role)}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function urlKey(url) {
|
|
166
|
+
return `url:${String(url || '').trim()}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function slugPart(value) {
|
|
170
|
+
const slug = String(value || 'unknown')
|
|
171
|
+
.toLowerCase()
|
|
172
|
+
.replace(/&/g, ' and ')
|
|
173
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
174
|
+
.replace(/^-+|-+$/g, '');
|
|
175
|
+
return slug || 'unknown';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function relativePath(projectDir, value) {
|
|
179
|
+
const text = String(value || '');
|
|
180
|
+
if (!text) return '';
|
|
181
|
+
const rel = relative(projectDir, text);
|
|
182
|
+
if (rel && !rel.startsWith('..') && !isAbsolute(rel)) return rel.replace(/\\/g, '/');
|
|
183
|
+
return text.replace(/\\/g, '/');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function compactObject(obj) {
|
|
187
|
+
const out = {};
|
|
188
|
+
for (const [key, value] of Object.entries(obj || {})) {
|
|
189
|
+
const clean = jsonValue(value);
|
|
190
|
+
if (clean !== undefined) out[key] = clean;
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function jsonValue(value) {
|
|
196
|
+
if (value === undefined) return undefined;
|
|
197
|
+
if (value === null) return null;
|
|
198
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
|
|
199
|
+
if (Array.isArray(value)) return value.map(jsonValue).filter((item) => item !== undefined);
|
|
200
|
+
if (typeof value === 'object') return compactObject(value);
|
|
201
|
+
return String(value);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function stringOrEmpty(value) {
|
|
205
|
+
if (value === undefined || value === null) return undefined;
|
|
206
|
+
const text = String(value);
|
|
207
|
+
return text.length > 0 ? text : undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function numberOrString(value) {
|
|
211
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
212
|
+
const number = Number(value);
|
|
213
|
+
return Number.isFinite(number) && String(value).trim() !== '' ? number : String(value);
|
|
214
|
+
}
|
package/merge-tracker.mjs
CHANGED
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
import {
|
|
30
30
|
DEFAULT_STATES, loadCanonicalStates, buildStatusDetectionRegex,
|
|
31
31
|
} from './lib/canonical-states.mjs';
|
|
32
|
+
import { recordTrackerMergeResult } from './lib/jobforge-ledger.mjs';
|
|
33
|
+
import { formatContractIssues, parseTrackerRow } from './lib/jobforge-contracts.mjs';
|
|
32
34
|
|
|
33
35
|
const ADDITIONS_DIR = join(PROJECT_DIR, 'batch/tracker-additions');
|
|
34
36
|
const MERGED_DIR = join(ADDITIONS_DIR, 'merged');
|
|
@@ -155,64 +157,22 @@ function parseTsvContent(content, filename) {
|
|
|
155
157
|
content = content.trim();
|
|
156
158
|
if (!content) return null;
|
|
157
159
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
const format = detectTrackerRowFormat(content);
|
|
161
|
+
const parsed = parseTrackerRow(content, format, {
|
|
162
|
+
projectDir: PROJECT_DIR,
|
|
163
|
+
normalizeStatus: validateStatus,
|
|
164
|
+
});
|
|
160
165
|
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
console.warn(`⚠️ Skipping malformed pipe-delimited ${filename}: ${parts.length} fields`);
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
addition = {
|
|
168
|
-
num: parseInt(parts[0]),
|
|
169
|
-
date: parts[1],
|
|
170
|
-
company: parts[2],
|
|
171
|
-
role: parts[3],
|
|
172
|
-
score: parts[4],
|
|
173
|
-
status: validateStatus(parts[5]),
|
|
174
|
-
pdf: parts[6],
|
|
175
|
-
report: parts[7],
|
|
176
|
-
notes: parts[8] || '',
|
|
177
|
-
};
|
|
178
|
-
} else {
|
|
179
|
-
parts = content.split('\t');
|
|
180
|
-
if (parts.length < 8) {
|
|
181
|
-
console.warn(`⚠️ Skipping malformed TSV ${filename}: ${parts.length} fields`);
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const col4 = parts[4].trim();
|
|
186
|
-
const col5 = parts[5].trim();
|
|
187
|
-
const col4LooksLikeScore = /^\d+\.?\d*\/5$/.test(col4) || col4 === 'N/A' || col4 === 'DUP';
|
|
188
|
-
const col5LooksLikeScore = /^\d+\.?\d*\/5$/.test(col5) || col5 === 'N/A' || col5 === 'DUP';
|
|
189
|
-
const col4LooksLikeStatus = STATUS_DETECT_RE.test(col4);
|
|
190
|
-
const col5LooksLikeStatus = STATUS_DETECT_RE.test(col5);
|
|
191
|
-
|
|
192
|
-
let statusCol, scoreCol;
|
|
193
|
-
if (col4LooksLikeStatus && !col4LooksLikeScore) {
|
|
194
|
-
statusCol = col4; scoreCol = col5;
|
|
195
|
-
} else if (col4LooksLikeScore && col5LooksLikeStatus) {
|
|
196
|
-
statusCol = col5; scoreCol = col4;
|
|
197
|
-
} else if (col5LooksLikeScore && !col4LooksLikeScore) {
|
|
198
|
-
statusCol = col4; scoreCol = col5;
|
|
199
|
-
} else {
|
|
200
|
-
statusCol = col4; scoreCol = col5;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
addition = {
|
|
204
|
-
num: parseInt(parts[0]),
|
|
205
|
-
date: parts[1],
|
|
206
|
-
company: parts[2],
|
|
207
|
-
role: parts[3],
|
|
208
|
-
status: validateStatus(statusCol),
|
|
209
|
-
score: scoreCol,
|
|
210
|
-
pdf: parts[6],
|
|
211
|
-
report: parts[7],
|
|
212
|
-
notes: parts[8] || '',
|
|
213
|
-
};
|
|
166
|
+
if (!parsed.validation.ok) {
|
|
167
|
+
console.warn(`⚠️ Skipping ${filename}: tracker contract failed (${format}) — ${formatContractIssues(parsed.validation)}`);
|
|
168
|
+
return null;
|
|
214
169
|
}
|
|
215
170
|
|
|
171
|
+
const addition = {
|
|
172
|
+
...parsed.validation.record,
|
|
173
|
+
num: Number(parsed.validation.record.num),
|
|
174
|
+
};
|
|
175
|
+
|
|
216
176
|
if (isNaN(addition.num) || addition.num === 0) {
|
|
217
177
|
console.warn(`⚠️ Skipping ${filename}: invalid entry number`);
|
|
218
178
|
return null;
|
|
@@ -221,6 +181,18 @@ function parseTsvContent(content, filename) {
|
|
|
221
181
|
return addition;
|
|
222
182
|
}
|
|
223
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
|
+
|
|
224
196
|
// ---- Main ----
|
|
225
197
|
|
|
226
198
|
if (!existsSync(ADDITIONS_DIR)) {
|
|
@@ -288,6 +260,7 @@ let added = 0;
|
|
|
288
260
|
let updated = 0;
|
|
289
261
|
let skipped = 0;
|
|
290
262
|
const newEntries = [];
|
|
263
|
+
const ledgerRecords = [];
|
|
291
264
|
|
|
292
265
|
for (const file of tsvFiles) {
|
|
293
266
|
const content = readFileSync(join(ADDITIONS_DIR, file), 'utf-8').trim();
|
|
@@ -359,12 +332,15 @@ for (const file of tsvFiles) {
|
|
|
359
332
|
}
|
|
360
333
|
}
|
|
361
334
|
updated++;
|
|
335
|
+
ledgerRecords.push({ addition, outcome: 'updated', sourceFile: join(ADDITIONS_DIR, file), duplicateNum: duplicate.num, reason });
|
|
362
336
|
} else if (statusRegresses) {
|
|
363
337
|
console.log(`⏭️ Skip: ${addition.company} — ${addition.role} (existing #${duplicate.num} status ${duplicate.status} outranks new ${addition.status})`);
|
|
364
338
|
skipped++;
|
|
339
|
+
ledgerRecords.push({ addition, outcome: 'skipped', sourceFile: join(ADDITIONS_DIR, file), duplicateNum: duplicate.num, reason: 'status-regression' });
|
|
365
340
|
} else {
|
|
366
341
|
console.log(`⏭️ Skip: ${addition.company} — ${addition.role} (existing #${duplicate.num} ${oldScore} >= new ${newScore})`);
|
|
367
342
|
skipped++;
|
|
343
|
+
ledgerRecords.push({ addition, outcome: 'skipped', sourceFile: join(ADDITIONS_DIR, file), duplicateNum: duplicate.num, reason: 'no-improvement' });
|
|
368
344
|
}
|
|
369
345
|
} else {
|
|
370
346
|
const entryNum = addition.num > maxNum ? addition.num : ++maxNum;
|
|
@@ -376,6 +352,7 @@ for (const file of tsvFiles) {
|
|
|
376
352
|
});
|
|
377
353
|
added++;
|
|
378
354
|
console.log(`➕ Add #${entryNum}: ${addition.company} — ${addition.role} (${addition.score})`);
|
|
355
|
+
ledgerRecords.push({ addition: { ...addition, num: entryNum }, outcome: 'added', sourceFile: join(ADDITIONS_DIR, file), reason: 'new-entry' });
|
|
379
356
|
}
|
|
380
357
|
}
|
|
381
358
|
|
|
@@ -410,6 +387,23 @@ if (!DRY_RUN) {
|
|
|
410
387
|
renameSync(join(ADDITIONS_DIR, file), join(MERGED_DIR, file));
|
|
411
388
|
}
|
|
412
389
|
console.log(`\n✅ Moved ${tsvFiles.length} TSVs to merged/`);
|
|
390
|
+
|
|
391
|
+
let ledgerEvents = 0;
|
|
392
|
+
for (const record of ledgerRecords) {
|
|
393
|
+
try {
|
|
394
|
+
const result = recordTrackerMergeResult(record.addition, {
|
|
395
|
+
projectDir: PROJECT_DIR,
|
|
396
|
+
sourceFile: record.sourceFile,
|
|
397
|
+
outcome: record.outcome,
|
|
398
|
+
duplicateNum: record.duplicateNum,
|
|
399
|
+
reason: record.reason,
|
|
400
|
+
});
|
|
401
|
+
if (result.appended) ledgerEvents++;
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.warn(`⚠️ Could not append ledger event for ${record.sourceFile}: ${error instanceof Error ? error.message : String(error)}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
console.log(`🧾 Ledger: ${ledgerEvents} event(s) appended`);
|
|
413
407
|
}
|
|
414
408
|
|
|
415
409
|
console.log(`\n📊 Summary: +${added} added, 🔄${updated} updated, ⏭️${skipped} skipped`);
|
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.17",
|
|
4
4
|
"description": "AI-powered job search pipeline built on opencode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
"telemetry:watch": "node bin/job-forge.mjs telemetry:watch",
|
|
28
28
|
"guard:audit": "node bin/job-forge.mjs guard:audit",
|
|
29
29
|
"guard:explain": "node bin/job-forge.mjs guard:explain",
|
|
30
|
+
"ledger:status": "node bin/job-forge.mjs ledger:status",
|
|
31
|
+
"ledger:rebuild": "node bin/job-forge.mjs ledger:rebuild",
|
|
32
|
+
"ledger:verify": "node bin/job-forge.mjs ledger:verify",
|
|
33
|
+
"ledger:has": "node bin/job-forge.mjs ledger:has",
|
|
34
|
+
"ledger:query": "node bin/job-forge.mjs ledger:query",
|
|
30
35
|
"plan": "iso plan .",
|
|
31
36
|
"lint:agentmd": "agentmd lint iso/instructions.md",
|
|
32
37
|
"lint:modes": "isolint lint modes/",
|
|
@@ -91,7 +96,9 @@
|
|
|
91
96
|
"node": ">=20.6.0"
|
|
92
97
|
},
|
|
93
98
|
"dependencies": {
|
|
99
|
+
"@razroo/iso-contract": "^0.1.0",
|
|
94
100
|
"@razroo/iso-guard": "^0.1.0",
|
|
101
|
+
"@razroo/iso-ledger": "^0.1.0",
|
|
95
102
|
"@razroo/iso-orchestrator": "^0.1.0",
|
|
96
103
|
"@razroo/iso-trace": "^0.4.0",
|
|
97
104
|
"playwright": "^1.58.1"
|