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.
@@ -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
+ }
@@ -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`);