job-forge 2.14.21 → 2.14.23
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 +11 -5
- package/.opencode/skills/job-forge.md +10 -0
- package/AGENTS.md +11 -5
- package/CLAUDE.md +11 -5
- package/README.md +7 -3
- package/bin/create-job-forge.mjs +7 -0
- package/bin/job-forge.mjs +70 -0
- package/docs/ARCHITECTURE.md +6 -2
- package/docs/CUSTOMIZATION.md +4 -0
- package/docs/SETUP.md +4 -0
- package/iso/commands/job-forge.md +10 -0
- package/iso/instructions.md +11 -5
- package/lib/jobforge-cache.mjs +105 -0
- package/lib/jobforge-index.mjs +92 -0
- package/modes/auto-pipeline.md +3 -1
- package/package.json +20 -2
- package/scripts/cache.mjs +313 -0
- package/scripts/index.mjs +210 -0
- package/templates/capabilities.json +5 -1
- package/templates/index.json +144 -0
- package/verify-pipeline.mjs +20 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { relative } from 'path';
|
|
4
|
+
import {
|
|
5
|
+
formatBuildResult,
|
|
6
|
+
formatConfigSummary,
|
|
7
|
+
formatIndexRecords,
|
|
8
|
+
formatVerifyResult,
|
|
9
|
+
} from '@razroo/iso-index';
|
|
10
|
+
import { PROJECT_DIR } from '../tracker-lib.mjs';
|
|
11
|
+
import {
|
|
12
|
+
buildJobForgeIndex,
|
|
13
|
+
hasJobForgeIndexRecord,
|
|
14
|
+
indexExists,
|
|
15
|
+
jobForgeIndexConfigPath,
|
|
16
|
+
jobForgeIndexPath,
|
|
17
|
+
jobForgeIndexSummary,
|
|
18
|
+
queryJobForgeIndex,
|
|
19
|
+
readJobForgeIndexConfig,
|
|
20
|
+
verifyJobForgeIndex,
|
|
21
|
+
} from '../lib/jobforge-index.mjs';
|
|
22
|
+
|
|
23
|
+
const USAGE = `job-forge index - local deterministic artifact lookup
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
job-forge index:status [--json]
|
|
27
|
+
job-forge index:build [--json]
|
|
28
|
+
job-forge index:query [text] [--kind <kind>] [--key <key>] [--value <value>] [--source <path>] [--limit N] [--no-rebuild] [--json]
|
|
29
|
+
job-forge index:has [text] [--kind <kind>] [--key <key>] [--value <value>] [--source <path>] [--no-rebuild] [--json]
|
|
30
|
+
job-forge index:verify [--no-rebuild] [--json]
|
|
31
|
+
job-forge index:explain [--json]
|
|
32
|
+
job-forge index:path
|
|
33
|
+
|
|
34
|
+
Default config is templates/index.json. Default output is .jobforge-index.json.
|
|
35
|
+
Query, has, and verify rebuild the index by default so consumer projects need no
|
|
36
|
+
manual setup. Use --no-rebuild to inspect the existing index file only.`;
|
|
37
|
+
|
|
38
|
+
const [cmd = 'help', ...rawArgs] = process.argv.slice(2);
|
|
39
|
+
const opts = parseArgs(rawArgs);
|
|
40
|
+
|
|
41
|
+
if (opts.help || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
42
|
+
console.log(USAGE);
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
if (cmd === 'path') {
|
|
48
|
+
console.log(jobForgeIndexPath(PROJECT_DIR));
|
|
49
|
+
} else if (cmd === 'status') {
|
|
50
|
+
status(opts);
|
|
51
|
+
} else if (cmd === 'build') {
|
|
52
|
+
build(opts);
|
|
53
|
+
} else if (cmd === 'query') {
|
|
54
|
+
query(opts);
|
|
55
|
+
} else if (cmd === 'has') {
|
|
56
|
+
has(opts);
|
|
57
|
+
} else if (cmd === 'verify') {
|
|
58
|
+
verify(opts);
|
|
59
|
+
} else if (cmd === 'explain') {
|
|
60
|
+
explain(opts);
|
|
61
|
+
} else {
|
|
62
|
+
console.error(`unknown index command "${cmd}"\n`);
|
|
63
|
+
console.error(USAGE);
|
|
64
|
+
process.exit(2);
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseArgs(args) {
|
|
72
|
+
const opts = {
|
|
73
|
+
json: false,
|
|
74
|
+
help: false,
|
|
75
|
+
rebuild: true,
|
|
76
|
+
query: {},
|
|
77
|
+
text: [],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < args.length; i++) {
|
|
81
|
+
const arg = args[i];
|
|
82
|
+
if (arg === '--json') {
|
|
83
|
+
opts.json = true;
|
|
84
|
+
} else if (arg === '--no-rebuild') {
|
|
85
|
+
opts.rebuild = false;
|
|
86
|
+
} else if (arg === '--rebuild') {
|
|
87
|
+
opts.rebuild = true;
|
|
88
|
+
} else if (arg === '--kind') {
|
|
89
|
+
opts.query.kind = valueAfter(args, ++i, '--kind');
|
|
90
|
+
} else if (arg.startsWith('--kind=')) {
|
|
91
|
+
opts.query.kind = arg.slice('--kind='.length);
|
|
92
|
+
} else if (arg === '--key') {
|
|
93
|
+
opts.query.key = valueAfter(args, ++i, '--key');
|
|
94
|
+
} else if (arg.startsWith('--key=')) {
|
|
95
|
+
opts.query.key = arg.slice('--key='.length);
|
|
96
|
+
} else if (arg === '--value') {
|
|
97
|
+
opts.query.value = valueAfter(args, ++i, '--value');
|
|
98
|
+
} else if (arg.startsWith('--value=')) {
|
|
99
|
+
opts.query.value = arg.slice('--value='.length);
|
|
100
|
+
} else if (arg === '--source') {
|
|
101
|
+
opts.query.source = valueAfter(args, ++i, '--source');
|
|
102
|
+
} else if (arg.startsWith('--source=')) {
|
|
103
|
+
opts.query.source = arg.slice('--source='.length);
|
|
104
|
+
} else if (arg === '--limit') {
|
|
105
|
+
opts.query.limit = parsePositiveInteger(valueAfter(args, ++i, '--limit'), '--limit');
|
|
106
|
+
} else if (arg.startsWith('--limit=')) {
|
|
107
|
+
opts.query.limit = parsePositiveInteger(arg.slice('--limit='.length), '--limit');
|
|
108
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
109
|
+
opts.help = true;
|
|
110
|
+
} else if (arg.startsWith('--')) {
|
|
111
|
+
throw new Error(`unknown flag "${arg}"`);
|
|
112
|
+
} else {
|
|
113
|
+
opts.text.push(arg);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (opts.text.length > 0) opts.query.text = opts.text.join(' ');
|
|
118
|
+
return opts;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function valueAfter(values, index, flag) {
|
|
122
|
+
const value = values[index];
|
|
123
|
+
if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parsePositiveInteger(value, flag) {
|
|
128
|
+
const parsed = Number(value);
|
|
129
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${flag} must be a positive integer`);
|
|
130
|
+
return parsed;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function status(opts) {
|
|
134
|
+
const summary = jobForgeIndexSummary(PROJECT_DIR);
|
|
135
|
+
if (opts.json) {
|
|
136
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!summary.exists) {
|
|
140
|
+
console.log(`index: missing (${relativePath(summary.path)})`);
|
|
141
|
+
console.log('run: job-forge index:build');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const result = verifyJobForgeIndex({ rebuild: false }, PROJECT_DIR);
|
|
145
|
+
console.log(`index: ${relativePath(summary.path)}`);
|
|
146
|
+
console.log(`config: ${relativePath(summary.config)}`);
|
|
147
|
+
console.log(`sources: ${summary.sources}`);
|
|
148
|
+
console.log(`files: ${summary.files}`);
|
|
149
|
+
console.log(`records: ${summary.records}`);
|
|
150
|
+
console.log(`verify: ${result.ok ? 'PASS' : 'FAIL'} (${result.issues.length} issue(s))`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function build(opts) {
|
|
154
|
+
const { index, out } = buildJobForgeIndex({}, PROJECT_DIR);
|
|
155
|
+
if (opts.json) {
|
|
156
|
+
console.log(JSON.stringify({ out, stats: index.stats }, null, 2));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
console.log(formatBuildResult(index, out));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function query(opts) {
|
|
163
|
+
const records = queryJobForgeIndex(opts.query, { rebuild: opts.rebuild }, PROJECT_DIR);
|
|
164
|
+
if (opts.json) {
|
|
165
|
+
console.log(JSON.stringify(records, null, 2));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
console.log(formatIndexRecords(records));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function has(opts) {
|
|
172
|
+
const hit = hasJobForgeIndexRecord(opts.query, { rebuild: opts.rebuild }, PROJECT_DIR);
|
|
173
|
+
if (opts.json) {
|
|
174
|
+
console.log(JSON.stringify({ hit, query: opts.query }, null, 2));
|
|
175
|
+
} else {
|
|
176
|
+
console.log(hit ? 'MATCH' : 'MISS');
|
|
177
|
+
}
|
|
178
|
+
process.exit(hit ? 0 : 1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function verify(opts) {
|
|
182
|
+
if (!opts.rebuild && !indexExists(PROJECT_DIR)) {
|
|
183
|
+
if (opts.json) {
|
|
184
|
+
console.log(JSON.stringify({ ok: true, missing: true, path: jobForgeIndexPath(PROJECT_DIR) }, null, 2));
|
|
185
|
+
} else {
|
|
186
|
+
console.log(`index: missing (${relativePath(jobForgeIndexPath(PROJECT_DIR))})`);
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const result = verifyJobForgeIndex({ rebuild: opts.rebuild }, PROJECT_DIR);
|
|
191
|
+
if (opts.json) {
|
|
192
|
+
console.log(JSON.stringify(result, null, 2));
|
|
193
|
+
} else {
|
|
194
|
+
console.log(formatVerifyResult(result));
|
|
195
|
+
}
|
|
196
|
+
process.exit(result.ok ? 0 : 1);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function explain(opts) {
|
|
200
|
+
const config = readJobForgeIndexConfig(PROJECT_DIR);
|
|
201
|
+
if (opts.json) {
|
|
202
|
+
console.log(JSON.stringify(config, null, 2));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
console.log(formatConfigSummary(config));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function relativePath(path) {
|
|
209
|
+
return relative(PROJECT_DIR, path) || '.';
|
|
210
|
+
}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
"npx job-forge ledger:*",
|
|
12
12
|
"npx job-forge capabilities:*",
|
|
13
13
|
"npx job-forge context:*",
|
|
14
|
+
"npx job-forge cache:*",
|
|
15
|
+
"npx job-forge index:*",
|
|
14
16
|
"rg *"
|
|
15
17
|
],
|
|
16
18
|
"deny": [
|
|
@@ -56,7 +58,9 @@
|
|
|
56
58
|
"npx job-forge merge",
|
|
57
59
|
"npx job-forge verify",
|
|
58
60
|
"npx job-forge ledger:*",
|
|
59
|
-
"npx job-forge capabilities:*"
|
|
61
|
+
"npx job-forge capabilities:*",
|
|
62
|
+
"npx job-forge cache:*",
|
|
63
|
+
"npx job-forge index:*"
|
|
60
64
|
],
|
|
61
65
|
"deny": [
|
|
62
66
|
"task *"
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"sources": [
|
|
4
|
+
{
|
|
5
|
+
"name": "reports",
|
|
6
|
+
"include": ["reports/*.md"],
|
|
7
|
+
"format": "text",
|
|
8
|
+
"rules": [
|
|
9
|
+
{
|
|
10
|
+
"kind": "jobforge.report.url",
|
|
11
|
+
"pattern": "^\\*\\*URL:\\*\\*\\s*(?<url>https?://\\S+)",
|
|
12
|
+
"flags": "i",
|
|
13
|
+
"key": "url:{url}",
|
|
14
|
+
"value": "{source}",
|
|
15
|
+
"fields": {
|
|
16
|
+
"url": "{url}",
|
|
17
|
+
"report": "{source}"
|
|
18
|
+
},
|
|
19
|
+
"tags": ["report", "url"]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"kind": "jobforge.report.score",
|
|
23
|
+
"pattern": "^\\*\\*Score:\\*\\*\\s*(?<score>[0-9.]+/5)",
|
|
24
|
+
"flags": "i",
|
|
25
|
+
"key": "report:{source}:score",
|
|
26
|
+
"value": "{score}",
|
|
27
|
+
"fields": {
|
|
28
|
+
"score": "{score}",
|
|
29
|
+
"report": "{source}"
|
|
30
|
+
},
|
|
31
|
+
"tags": ["report", "score"]
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "application-day-files",
|
|
37
|
+
"include": ["data/applications/*.md"],
|
|
38
|
+
"format": "markdown-table",
|
|
39
|
+
"records": [
|
|
40
|
+
{
|
|
41
|
+
"kind": "jobforge.application",
|
|
42
|
+
"key": "company-role:{Company|slug}:{Role|slug}",
|
|
43
|
+
"value": "{Status}",
|
|
44
|
+
"fields": ["#", "Date", "Company", "Role", "Score", "Status", "PDF", "Report", "Notes"],
|
|
45
|
+
"tags": ["tracker", "application"]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"kind": "jobforge.report-ref",
|
|
49
|
+
"key": "{Report}",
|
|
50
|
+
"value": "{Company} - {Role}",
|
|
51
|
+
"fields": {
|
|
52
|
+
"company": "{Company}",
|
|
53
|
+
"role": "{Role}",
|
|
54
|
+
"status": "{Status}",
|
|
55
|
+
"report": "{Report}"
|
|
56
|
+
},
|
|
57
|
+
"tags": ["tracker", "report"]
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "application-single-file",
|
|
63
|
+
"include": ["data/applications.md", "applications.md"],
|
|
64
|
+
"format": "markdown-table",
|
|
65
|
+
"records": [
|
|
66
|
+
{
|
|
67
|
+
"kind": "jobforge.application",
|
|
68
|
+
"key": "company-role:{Company|slug}:{Role|slug}",
|
|
69
|
+
"value": "{Status}",
|
|
70
|
+
"fields": ["#", "Date", "Company", "Role", "Score", "Status", "PDF", "Report", "Notes"],
|
|
71
|
+
"tags": ["tracker", "application"]
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"name": "tracker-additions",
|
|
77
|
+
"include": ["batch/tracker-additions/*.tsv", "batch/tracker-additions/merged/*.tsv"],
|
|
78
|
+
"format": "tsv",
|
|
79
|
+
"header": false,
|
|
80
|
+
"columns": ["num", "date", "company", "role", "statusOrScore", "scoreOrStatus", "pdf", "report", "notes"],
|
|
81
|
+
"records": [
|
|
82
|
+
{
|
|
83
|
+
"kind": "jobforge.tracker-addition",
|
|
84
|
+
"key": "company-role:{company|slug}:{role|slug}",
|
|
85
|
+
"value": "{source}",
|
|
86
|
+
"fields": ["num", "date", "company", "role", "statusOrScore", "scoreOrStatus", "pdf", "report", "notes"],
|
|
87
|
+
"tags": ["tracker", "tsv"]
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"name": "pipeline",
|
|
93
|
+
"include": ["data/pipeline.md"],
|
|
94
|
+
"format": "text",
|
|
95
|
+
"rules": [
|
|
96
|
+
{
|
|
97
|
+
"kind": "jobforge.pipeline.url",
|
|
98
|
+
"pattern": "^\\s*-\\s*\\[(?<state>[ xX])\\]\\s+(?<url>https?://\\S+)",
|
|
99
|
+
"key": "url:{url}",
|
|
100
|
+
"value": "{state}",
|
|
101
|
+
"fields": {
|
|
102
|
+
"url": "{url}",
|
|
103
|
+
"state": "{state}",
|
|
104
|
+
"pipeline": "{source}"
|
|
105
|
+
},
|
|
106
|
+
"tags": ["pipeline", "url"]
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"name": "scan-history",
|
|
112
|
+
"include": ["data/scan-history.tsv"],
|
|
113
|
+
"format": "tsv",
|
|
114
|
+
"records": [
|
|
115
|
+
{
|
|
116
|
+
"kind": "jobforge.scan.url",
|
|
117
|
+
"key": "url:{url}",
|
|
118
|
+
"value": "{company} - {role}",
|
|
119
|
+
"fields": ["date", "company", "role", "url", "ats"],
|
|
120
|
+
"tags": ["scan", "url"]
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"name": "ledger",
|
|
126
|
+
"include": [".jobforge-ledger/*.jsonl"],
|
|
127
|
+
"format": "jsonl",
|
|
128
|
+
"records": [
|
|
129
|
+
{
|
|
130
|
+
"kind": "jobforge.ledger.event",
|
|
131
|
+
"key": "{key}",
|
|
132
|
+
"value": "{type}",
|
|
133
|
+
"fields": {
|
|
134
|
+
"type": "{type}",
|
|
135
|
+
"key": "{key}",
|
|
136
|
+
"status": "{data.status}",
|
|
137
|
+
"source": "{source}"
|
|
138
|
+
},
|
|
139
|
+
"tags": ["ledger"]
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
}
|
package/verify-pipeline.mjs
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* 8. No markdown bold in score column
|
|
18
18
|
* 9. Drift warning if states.yml ids differ from the built-in fallback list
|
|
19
19
|
* 10. Ledger file verifies if .jobforge-ledger/events.jsonl exists
|
|
20
|
+
* 11. Artifact index verifies if .jobforge-index.json exists
|
|
20
21
|
*
|
|
21
22
|
* Run: node verify-pipeline.mjs (from repo root; same as npm run verify)
|
|
22
23
|
*/
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
usesDayFiles, readAllEntries, listDayFiles, dayFilePath,
|
|
30
31
|
} from './tracker-lib.mjs';
|
|
31
32
|
import { jobForgeLedgerPath, ledgerExists, verifyJobForgeLedger } from './lib/jobforge-ledger.mjs';
|
|
33
|
+
import { indexExists, jobForgeIndexPath, verifyJobForgeIndex } from './lib/jobforge-index.mjs';
|
|
32
34
|
import {
|
|
33
35
|
canonicalStatusValues,
|
|
34
36
|
formatContractIssues,
|
|
@@ -153,6 +155,22 @@ function verifyLedgerIfPresent() {
|
|
|
153
155
|
}
|
|
154
156
|
}
|
|
155
157
|
|
|
158
|
+
function verifyIndexIfPresent() {
|
|
159
|
+
if (!indexExists(PROJECT_DIR)) {
|
|
160
|
+
ok('Artifact index not initialized');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const result = verifyJobForgeIndex({ rebuild: false }, PROJECT_DIR);
|
|
164
|
+
for (const issue of result.issues) {
|
|
165
|
+
const msg = `index: ${issue.kind}: ${issue.message}`;
|
|
166
|
+
if (issue.severity === 'error') error(msg);
|
|
167
|
+
else warn(msg);
|
|
168
|
+
}
|
|
169
|
+
if (result.ok) {
|
|
170
|
+
ok(`Artifact index valid (${result.records} records at ${relative(PROJECT_DIR, jobForgeIndexPath(PROJECT_DIR))})`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
156
174
|
// --- Read entries ---
|
|
157
175
|
const { entries, source } = readAllEntries();
|
|
158
176
|
|
|
@@ -162,6 +180,7 @@ if (entries.length === 0) {
|
|
|
162
180
|
checkPendingTrackerAdditions();
|
|
163
181
|
verifyStatesYamlDrift();
|
|
164
182
|
verifyLedgerIfPresent();
|
|
183
|
+
verifyIndexIfPresent();
|
|
165
184
|
console.log('\n' + '='.repeat(50));
|
|
166
185
|
console.log(`📊 Pipeline Health: ${errors} errors, ${warnings} warnings`);
|
|
167
186
|
if (errors === 0 && warnings === 0) console.log('🟢 Pipeline is clean!');
|
|
@@ -297,6 +316,7 @@ if (boldScores === 0) ok('No bold in scores');
|
|
|
297
316
|
|
|
298
317
|
verifyStatesYamlDrift();
|
|
299
318
|
verifyLedgerIfPresent();
|
|
319
|
+
verifyIndexIfPresent();
|
|
300
320
|
|
|
301
321
|
console.log('\n' + '='.repeat(50));
|
|
302
322
|
console.log(`📊 Pipeline Health: ${errors} errors, ${warnings} warnings`);
|