job-forge 2.14.14 → 2.14.16
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 +6 -3
- package/.opencode/skills/job-forge.md +7 -0
- package/AGENTS.md +6 -3
- package/CLAUDE.md +6 -3
- package/README.md +5 -1
- package/bin/create-job-forge.mjs +11 -1
- package/bin/job-forge.mjs +55 -0
- package/docs/ARCHITECTURE.md +7 -1
- package/docs/CUSTOMIZATION.md +25 -0
- package/docs/README.md +1 -1
- package/docs/SETUP.md +5 -0
- package/iso/commands/job-forge.md +7 -0
- package/iso/instructions.md +6 -3
- package/lib/jobforge-ledger.mjs +214 -0
- package/merge-tracker.mjs +23 -0
- package/package.json +10 -1
- package/scripts/guard.mjs +404 -0
- package/scripts/ledger.mjs +359 -0
- package/scripts/telemetry.mjs +14 -0
- package/scripts/tracker-line.mjs +8 -0
- package/templates/guards/jobforge-baseline.yaml +50 -0
- package/verify-pipeline.mjs +21 -0
|
@@ -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,7 @@ 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';
|
|
32
33
|
|
|
33
34
|
const ADDITIONS_DIR = join(PROJECT_DIR, 'batch/tracker-additions');
|
|
34
35
|
const MERGED_DIR = join(ADDITIONS_DIR, 'merged');
|
|
@@ -288,6 +289,7 @@ let added = 0;
|
|
|
288
289
|
let updated = 0;
|
|
289
290
|
let skipped = 0;
|
|
290
291
|
const newEntries = [];
|
|
292
|
+
const ledgerRecords = [];
|
|
291
293
|
|
|
292
294
|
for (const file of tsvFiles) {
|
|
293
295
|
const content = readFileSync(join(ADDITIONS_DIR, file), 'utf-8').trim();
|
|
@@ -359,12 +361,15 @@ for (const file of tsvFiles) {
|
|
|
359
361
|
}
|
|
360
362
|
}
|
|
361
363
|
updated++;
|
|
364
|
+
ledgerRecords.push({ addition, outcome: 'updated', sourceFile: join(ADDITIONS_DIR, file), duplicateNum: duplicate.num, reason });
|
|
362
365
|
} else if (statusRegresses) {
|
|
363
366
|
console.log(`⏭️ Skip: ${addition.company} — ${addition.role} (existing #${duplicate.num} status ${duplicate.status} outranks new ${addition.status})`);
|
|
364
367
|
skipped++;
|
|
368
|
+
ledgerRecords.push({ addition, outcome: 'skipped', sourceFile: join(ADDITIONS_DIR, file), duplicateNum: duplicate.num, reason: 'status-regression' });
|
|
365
369
|
} else {
|
|
366
370
|
console.log(`⏭️ Skip: ${addition.company} — ${addition.role} (existing #${duplicate.num} ${oldScore} >= new ${newScore})`);
|
|
367
371
|
skipped++;
|
|
372
|
+
ledgerRecords.push({ addition, outcome: 'skipped', sourceFile: join(ADDITIONS_DIR, file), duplicateNum: duplicate.num, reason: 'no-improvement' });
|
|
368
373
|
}
|
|
369
374
|
} else {
|
|
370
375
|
const entryNum = addition.num > maxNum ? addition.num : ++maxNum;
|
|
@@ -376,6 +381,7 @@ for (const file of tsvFiles) {
|
|
|
376
381
|
});
|
|
377
382
|
added++;
|
|
378
383
|
console.log(`➕ Add #${entryNum}: ${addition.company} — ${addition.role} (${addition.score})`);
|
|
384
|
+
ledgerRecords.push({ addition: { ...addition, num: entryNum }, outcome: 'added', sourceFile: join(ADDITIONS_DIR, file), reason: 'new-entry' });
|
|
379
385
|
}
|
|
380
386
|
}
|
|
381
387
|
|
|
@@ -410,6 +416,23 @@ if (!DRY_RUN) {
|
|
|
410
416
|
renameSync(join(ADDITIONS_DIR, file), join(MERGED_DIR, file));
|
|
411
417
|
}
|
|
412
418
|
console.log(`\n✅ Moved ${tsvFiles.length} TSVs to merged/`);
|
|
419
|
+
|
|
420
|
+
let ledgerEvents = 0;
|
|
421
|
+
for (const record of ledgerRecords) {
|
|
422
|
+
try {
|
|
423
|
+
const result = recordTrackerMergeResult(record.addition, {
|
|
424
|
+
projectDir: PROJECT_DIR,
|
|
425
|
+
sourceFile: record.sourceFile,
|
|
426
|
+
outcome: record.outcome,
|
|
427
|
+
duplicateNum: record.duplicateNum,
|
|
428
|
+
reason: record.reason,
|
|
429
|
+
});
|
|
430
|
+
if (result.appended) ledgerEvents++;
|
|
431
|
+
} catch (error) {
|
|
432
|
+
console.warn(`⚠️ Could not append ledger event for ${record.sourceFile}: ${error instanceof Error ? error.message : String(error)}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
console.log(`🧾 Ledger: ${ledgerEvents} event(s) appended`);
|
|
413
436
|
}
|
|
414
437
|
|
|
415
438
|
console.log(`\n📊 Summary: +${added} added, 🔄${updated} updated, ⏭️${skipped} skipped`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-forge",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.16",
|
|
4
4
|
"description": "AI-powered job search pipeline built on opencode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,6 +25,13 @@
|
|
|
25
25
|
"telemetry:status": "node bin/job-forge.mjs telemetry:status",
|
|
26
26
|
"telemetry:show": "node bin/job-forge.mjs telemetry:show",
|
|
27
27
|
"telemetry:watch": "node bin/job-forge.mjs telemetry:watch",
|
|
28
|
+
"guard:audit": "node bin/job-forge.mjs guard:audit",
|
|
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",
|
|
28
35
|
"plan": "iso plan .",
|
|
29
36
|
"lint:agentmd": "agentmd lint iso/instructions.md",
|
|
30
37
|
"lint:modes": "isolint lint modes/",
|
|
@@ -89,6 +96,8 @@
|
|
|
89
96
|
"node": ">=20.6.0"
|
|
90
97
|
},
|
|
91
98
|
"dependencies": {
|
|
99
|
+
"@razroo/iso-guard": "^0.1.0",
|
|
100
|
+
"@razroo/iso-ledger": "^0.1.0",
|
|
92
101
|
"@razroo/iso-orchestrator": "^0.1.0",
|
|
93
102
|
"@razroo/iso-trace": "^0.4.0",
|
|
94
103
|
"playwright": "^1.58.1"
|