job-forge 2.14.30 → 2.14.32

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,238 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { relative } from 'path';
4
+ import {
5
+ formatBuildResult,
6
+ formatCheckResult,
7
+ formatConfigSummary,
8
+ formatFacts,
9
+ formatVerifyResult,
10
+ } from '@razroo/iso-facts';
11
+ import { PROJECT_DIR } from '../tracker-lib.mjs';
12
+ import {
13
+ buildJobForgeFacts,
14
+ checkJobForgeFacts,
15
+ factsExist,
16
+ hasJobForgeFact,
17
+ jobForgeFactsConfigPath,
18
+ jobForgeFactsPath,
19
+ jobForgeFactsSummary,
20
+ queryJobForgeFacts,
21
+ readJobForgeFactsConfig,
22
+ verifyJobForgeFacts,
23
+ } from '../lib/jobforge-facts.mjs';
24
+
25
+ const USAGE = `job-forge facts - local deterministic fact materialization
26
+
27
+ Usage:
28
+ job-forge facts:status [--json]
29
+ job-forge facts:build [--json]
30
+ job-forge facts:query [text] [--fact <fact>] [--key <key>] [--value <value>] [--source <path>] [--tag <tag>] [--limit N] [--no-rebuild] [--json]
31
+ job-forge facts:has [text] [--fact <fact>] [--key <key>] [--value <value>] [--source <path>] [--tag <tag>] [--no-rebuild] [--json]
32
+ job-forge facts:verify [--no-rebuild] [--json]
33
+ job-forge facts:check [--no-rebuild] [--json]
34
+ job-forge facts:explain [--json]
35
+ job-forge facts:path
36
+
37
+ Default config is templates/facts.json. Default output is .jobforge-facts.json.
38
+ Query, has, verify, and check rebuild facts by default so consumer projects need
39
+ no manual setup. Use --no-rebuild to inspect the existing fact set only.`;
40
+
41
+ const [cmd = 'help', ...rawArgs] = process.argv.slice(2);
42
+ const opts = parseArgs(rawArgs);
43
+
44
+ if (opts.help || cmd === 'help' || cmd === '--help' || cmd === '-h') {
45
+ console.log(USAGE);
46
+ process.exit(0);
47
+ }
48
+
49
+ try {
50
+ if (cmd === 'path') {
51
+ console.log(jobForgeFactsPath(PROJECT_DIR));
52
+ } else if (cmd === 'status') {
53
+ status(opts);
54
+ } else if (cmd === 'build') {
55
+ build(opts);
56
+ } else if (cmd === 'query') {
57
+ query(opts);
58
+ } else if (cmd === 'has') {
59
+ has(opts);
60
+ } else if (cmd === 'verify') {
61
+ verify(opts);
62
+ } else if (cmd === 'check') {
63
+ check(opts);
64
+ } else if (cmd === 'explain') {
65
+ explain(opts);
66
+ } else {
67
+ console.error(`unknown facts command "${cmd}"\n`);
68
+ console.error(USAGE);
69
+ process.exit(2);
70
+ }
71
+ } catch (error) {
72
+ console.error(error instanceof Error ? error.message : String(error));
73
+ process.exit(1);
74
+ }
75
+
76
+ function parseArgs(args) {
77
+ const opts = {
78
+ json: false,
79
+ help: false,
80
+ rebuild: true,
81
+ query: {},
82
+ text: [],
83
+ };
84
+
85
+ for (let i = 0; i < args.length; i++) {
86
+ const arg = args[i];
87
+ if (arg === '--json') {
88
+ opts.json = true;
89
+ } else if (arg === '--no-rebuild') {
90
+ opts.rebuild = false;
91
+ } else if (arg === '--rebuild') {
92
+ opts.rebuild = true;
93
+ } else if (arg === '--fact') {
94
+ opts.query.fact = valueAfter(args, ++i, '--fact');
95
+ } else if (arg.startsWith('--fact=')) {
96
+ opts.query.fact = arg.slice('--fact='.length);
97
+ } else if (arg === '--key') {
98
+ opts.query.key = valueAfter(args, ++i, '--key');
99
+ } else if (arg.startsWith('--key=')) {
100
+ opts.query.key = arg.slice('--key='.length);
101
+ } else if (arg === '--value') {
102
+ opts.query.value = valueAfter(args, ++i, '--value');
103
+ } else if (arg.startsWith('--value=')) {
104
+ opts.query.value = arg.slice('--value='.length);
105
+ } else if (arg === '--source') {
106
+ opts.query.source = valueAfter(args, ++i, '--source');
107
+ } else if (arg.startsWith('--source=')) {
108
+ opts.query.source = arg.slice('--source='.length);
109
+ } else if (arg === '--tag') {
110
+ opts.query.tag = valueAfter(args, ++i, '--tag');
111
+ } else if (arg.startsWith('--tag=')) {
112
+ opts.query.tag = arg.slice('--tag='.length);
113
+ } else if (arg === '--limit') {
114
+ opts.query.limit = parsePositiveInteger(valueAfter(args, ++i, '--limit'), '--limit');
115
+ } else if (arg.startsWith('--limit=')) {
116
+ opts.query.limit = parsePositiveInteger(arg.slice('--limit='.length), '--limit');
117
+ } else if (arg === '--help' || arg === '-h') {
118
+ opts.help = true;
119
+ } else if (arg.startsWith('--')) {
120
+ throw new Error(`unknown flag "${arg}"`);
121
+ } else {
122
+ opts.text.push(arg);
123
+ }
124
+ }
125
+
126
+ if (opts.text.length > 0) opts.query.text = opts.text.join(' ');
127
+ return opts;
128
+ }
129
+
130
+ function valueAfter(values, index, flag) {
131
+ const value = values[index];
132
+ if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
133
+ return value;
134
+ }
135
+
136
+ function parsePositiveInteger(value, flag) {
137
+ const parsed = Number(value);
138
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${flag} must be a positive integer`);
139
+ return parsed;
140
+ }
141
+
142
+ function status(opts) {
143
+ const summary = jobForgeFactsSummary(PROJECT_DIR);
144
+ if (opts.json) {
145
+ console.log(JSON.stringify(summary, null, 2));
146
+ return;
147
+ }
148
+ if (!summary.exists) {
149
+ console.log(`facts: missing (${relativePath(summary.path)})`);
150
+ console.log('run: job-forge facts:build');
151
+ return;
152
+ }
153
+ const result = verifyJobForgeFacts({ rebuild: false }, PROJECT_DIR);
154
+ console.log(`facts: ${relativePath(summary.path)}`);
155
+ console.log(`config: ${relativePath(summary.config)}`);
156
+ console.log(`sources: ${summary.sources}`);
157
+ console.log(`files: ${summary.files}`);
158
+ console.log(`facts: ${summary.facts}`);
159
+ console.log(`verify: ${result.ok ? 'PASS' : 'FAIL'} (${result.issues.length} issue(s))`);
160
+ }
161
+
162
+ function build(opts) {
163
+ const { factSet, out } = buildJobForgeFacts({}, PROJECT_DIR);
164
+ if (opts.json) {
165
+ console.log(JSON.stringify({ out, stats: factSet.stats }, null, 2));
166
+ return;
167
+ }
168
+ console.log(formatBuildResult(factSet, out));
169
+ }
170
+
171
+ function query(opts) {
172
+ const facts = queryJobForgeFacts(opts.query, { rebuild: opts.rebuild }, PROJECT_DIR);
173
+ if (opts.json) {
174
+ console.log(JSON.stringify(facts, null, 2));
175
+ return;
176
+ }
177
+ console.log(formatFacts(facts));
178
+ }
179
+
180
+ function has(opts) {
181
+ const hit = hasJobForgeFact(opts.query, { rebuild: opts.rebuild }, PROJECT_DIR);
182
+ if (opts.json) {
183
+ console.log(JSON.stringify({ hit, query: opts.query }, null, 2));
184
+ } else {
185
+ console.log(hit ? 'MATCH' : 'MISS');
186
+ }
187
+ process.exit(hit ? 0 : 1);
188
+ }
189
+
190
+ function verify(opts) {
191
+ if (!opts.rebuild && !factsExist(PROJECT_DIR)) {
192
+ if (opts.json) {
193
+ console.log(JSON.stringify({ ok: true, missing: true, path: jobForgeFactsPath(PROJECT_DIR) }, null, 2));
194
+ } else {
195
+ console.log(`facts: missing (${relativePath(jobForgeFactsPath(PROJECT_DIR))})`);
196
+ }
197
+ return;
198
+ }
199
+ const result = verifyJobForgeFacts({ rebuild: opts.rebuild }, PROJECT_DIR);
200
+ if (opts.json) {
201
+ console.log(JSON.stringify(result, null, 2));
202
+ } else {
203
+ console.log(formatVerifyResult(result));
204
+ }
205
+ process.exit(result.ok ? 0 : 1);
206
+ }
207
+
208
+ function check(opts) {
209
+ if (!opts.rebuild && !factsExist(PROJECT_DIR)) {
210
+ if (opts.json) {
211
+ console.log(JSON.stringify({ ok: true, missing: true, path: jobForgeFactsPath(PROJECT_DIR) }, null, 2));
212
+ } else {
213
+ console.log(`facts: missing (${relativePath(jobForgeFactsPath(PROJECT_DIR))})`);
214
+ }
215
+ return;
216
+ }
217
+ const result = checkJobForgeFacts({ rebuild: opts.rebuild }, PROJECT_DIR);
218
+ if (opts.json) {
219
+ console.log(JSON.stringify(result, null, 2));
220
+ } else {
221
+ console.log(formatCheckResult(result));
222
+ }
223
+ process.exit(result.ok ? 0 : 1);
224
+ }
225
+
226
+ function explain(opts) {
227
+ const config = readJobForgeFactsConfig(PROJECT_DIR);
228
+ if (opts.json) {
229
+ console.log(JSON.stringify(config, null, 2));
230
+ return;
231
+ }
232
+ console.log(`config: ${relativePath(jobForgeFactsConfigPath(PROJECT_DIR))}`);
233
+ console.log(formatConfigSummary(config));
234
+ }
235
+
236
+ function relativePath(path) {
237
+ return relative(PROJECT_DIR, path) || '.';
238
+ }
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { writeFileSync } from 'fs';
4
+ import { relative, resolve } from 'path';
5
+ import {
6
+ formatCheckResult,
7
+ formatComparison,
8
+ formatConfigSummary,
9
+ formatGateResult,
10
+ formatScoreResult,
11
+ formatVerifyResult,
12
+ } from '@razroo/iso-score';
13
+ import { PROJECT_DIR } from '../tracker-lib.mjs';
14
+ import {
15
+ checkJobForgeScore,
16
+ compareJobForgeScores,
17
+ computeJobForgeScore,
18
+ evaluateJobForgeScoreGate,
19
+ jobForgeScoreConfigPath,
20
+ readJobForgeScoreConfig,
21
+ readJsonFile,
22
+ verifyJobForgeScoreResult,
23
+ } from '../lib/jobforge-score.mjs';
24
+
25
+ const USAGE = `job-forge score - deterministic JobForge offer scoring
26
+
27
+ Usage:
28
+ job-forge score:compute --input <file> [--out <file>] [--profile jobforge] [--json]
29
+ job-forge score:check --input <file> [--profile jobforge] [--json]
30
+ job-forge score:gate --input <file> [--gate apply] [--profile jobforge] [--json]
31
+ job-forge score:verify --score <file> [--json]
32
+ job-forge score:compare --left <file> --right <file> [--profile jobforge] [--json]
33
+ job-forge score:explain [--profile jobforge] [--json]
34
+ job-forge score:path
35
+
36
+ Default config is templates/score.json. The input may be native iso-score
37
+ JSON or JobForge's existing report score JSON shape with a top-level scores map.`;
38
+
39
+ const [cmd = 'help', ...rawArgs] = process.argv.slice(2);
40
+ const opts = parseArgs(rawArgs);
41
+
42
+ if (opts.help || cmd === 'help' || cmd === '--help' || cmd === '-h') {
43
+ console.log(USAGE);
44
+ process.exit(0);
45
+ }
46
+
47
+ try {
48
+ if (cmd === 'path') {
49
+ console.log(jobForgeScoreConfigPath(PROJECT_DIR));
50
+ } else if (cmd === 'compute') {
51
+ compute(opts);
52
+ } else if (cmd === 'check') {
53
+ check(opts);
54
+ } else if (cmd === 'gate') {
55
+ gate(opts);
56
+ } else if (cmd === 'verify') {
57
+ verify(opts);
58
+ } else if (cmd === 'compare') {
59
+ compare(opts);
60
+ } else if (cmd === 'explain') {
61
+ explain(opts);
62
+ } else {
63
+ console.error(`unknown score command "${cmd}"\n`);
64
+ console.error(USAGE);
65
+ process.exit(2);
66
+ }
67
+ } catch (error) {
68
+ console.error(error instanceof Error ? error.message : String(error));
69
+ process.exit(1);
70
+ }
71
+
72
+ function parseArgs(args) {
73
+ const opts = {
74
+ json: false,
75
+ help: false,
76
+ profile: '',
77
+ gate: '',
78
+ input: '',
79
+ score: '',
80
+ left: '',
81
+ right: '',
82
+ out: '',
83
+ };
84
+
85
+ for (let i = 0; i < args.length; i++) {
86
+ const arg = args[i];
87
+ if (arg === '--json') {
88
+ opts.json = true;
89
+ } else if (arg === '--profile') {
90
+ opts.profile = valueAfter(args, ++i, '--profile');
91
+ } else if (arg.startsWith('--profile=')) {
92
+ opts.profile = arg.slice('--profile='.length);
93
+ } else if (arg === '--gate') {
94
+ opts.gate = valueAfter(args, ++i, '--gate');
95
+ } else if (arg.startsWith('--gate=')) {
96
+ opts.gate = arg.slice('--gate='.length);
97
+ } else if (arg === '--input') {
98
+ opts.input = valueAfter(args, ++i, '--input');
99
+ } else if (arg.startsWith('--input=')) {
100
+ opts.input = arg.slice('--input='.length);
101
+ } else if (arg === '--score') {
102
+ opts.score = valueAfter(args, ++i, '--score');
103
+ } else if (arg.startsWith('--score=')) {
104
+ opts.score = arg.slice('--score='.length);
105
+ } else if (arg === '--left') {
106
+ opts.left = valueAfter(args, ++i, '--left');
107
+ } else if (arg.startsWith('--left=')) {
108
+ opts.left = arg.slice('--left='.length);
109
+ } else if (arg === '--right') {
110
+ opts.right = valueAfter(args, ++i, '--right');
111
+ } else if (arg.startsWith('--right=')) {
112
+ opts.right = arg.slice('--right='.length);
113
+ } else if (arg === '--out') {
114
+ opts.out = valueAfter(args, ++i, '--out');
115
+ } else if (arg.startsWith('--out=')) {
116
+ opts.out = arg.slice('--out='.length);
117
+ } else if (arg === '--help' || arg === '-h') {
118
+ opts.help = true;
119
+ } else {
120
+ throw new Error(`unknown flag "${arg}"`);
121
+ }
122
+ }
123
+
124
+ return opts;
125
+ }
126
+
127
+ function valueAfter(values, index, flag) {
128
+ const value = values[index];
129
+ if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
130
+ return value;
131
+ }
132
+
133
+ function compute(opts) {
134
+ if (!opts.input) throw new Error('score:compute requires --input');
135
+ const result = computeJobForgeScore(readJsonFile(resolve(opts.input)), { profile: opts.profile }, PROJECT_DIR);
136
+ if (opts.out) writeFileSync(resolve(opts.out), `${JSON.stringify(result, null, 2)}\n`, 'utf8');
137
+ if (opts.json) {
138
+ console.log(JSON.stringify(result, null, 2));
139
+ } else {
140
+ console.log(formatScoreResult(result));
141
+ }
142
+ }
143
+
144
+ function check(opts) {
145
+ if (!opts.input) throw new Error('score:check requires --input');
146
+ const result = checkJobForgeScore(readJsonFile(resolve(opts.input)), { profile: opts.profile }, PROJECT_DIR);
147
+ if (opts.json) {
148
+ console.log(JSON.stringify(result, null, 2));
149
+ } else {
150
+ console.log(formatCheckResult(result));
151
+ }
152
+ process.exit(result.ok ? 0 : 1);
153
+ }
154
+
155
+ function gate(opts) {
156
+ if (!opts.input) throw new Error('score:gate requires --input');
157
+ const result = evaluateJobForgeScoreGate(readJsonFile(resolve(opts.input)), {
158
+ gate: opts.gate || 'apply',
159
+ profile: opts.profile,
160
+ }, PROJECT_DIR);
161
+ if (opts.json) {
162
+ console.log(JSON.stringify(result, null, 2));
163
+ } else {
164
+ console.log(formatGateResult(result));
165
+ }
166
+ process.exit(result.ok ? 0 : 1);
167
+ }
168
+
169
+ function verify(opts) {
170
+ if (!opts.score) throw new Error('score:verify requires --score');
171
+ const result = verifyJobForgeScoreResult(readJsonFile(resolve(opts.score)));
172
+ if (opts.json) {
173
+ console.log(JSON.stringify(result, null, 2));
174
+ } else {
175
+ console.log(formatVerifyResult(result));
176
+ }
177
+ process.exit(result.ok ? 0 : 1);
178
+ }
179
+
180
+ function compare(opts) {
181
+ if (!opts.left) throw new Error('score:compare requires --left');
182
+ if (!opts.right) throw new Error('score:compare requires --right');
183
+ const result = compareJobForgeScores(
184
+ readJsonFile(resolve(opts.left)),
185
+ readJsonFile(resolve(opts.right)),
186
+ { profile: opts.profile },
187
+ PROJECT_DIR,
188
+ );
189
+ if (opts.json) {
190
+ console.log(JSON.stringify(result, null, 2));
191
+ } else {
192
+ console.log(formatComparison(result));
193
+ }
194
+ const hasErrors = [...result.left.issues, ...result.right.issues].some((issue) => issue.severity === 'error');
195
+ process.exit(hasErrors ? 1 : 0);
196
+ }
197
+
198
+ function explain(opts) {
199
+ const config = readJobForgeScoreConfig(PROJECT_DIR);
200
+ if (opts.json) {
201
+ const value = opts.profile
202
+ ? { ...config, profiles: config.profiles.filter((profile) => profile.name === opts.profile) }
203
+ : config;
204
+ console.log(JSON.stringify(value, null, 2));
205
+ } else {
206
+ console.log(`config: ${relative(PROJECT_DIR, jobForgeScoreConfigPath(PROJECT_DIR))}`);
207
+ console.log(formatConfigSummary(config, opts.profile || undefined));
208
+ }
209
+ }
@@ -0,0 +1,200 @@
1
+ {
2
+ "version": 1,
3
+ "sources": [
4
+ {
5
+ "name": "reports",
6
+ "include": ["reports/*.md"],
7
+ "format": "text",
8
+ "rules": [
9
+ {
10
+ "fact": "job.url",
11
+ "pattern": "^\\*\\*URL:\\*\\*\\s*(?<url>https?://\\S+)",
12
+ "flags": "i",
13
+ "key": "url:{url}",
14
+ "value": "{url}",
15
+ "fields": {
16
+ "url": "{url}",
17
+ "report": "{source}"
18
+ },
19
+ "tags": ["report", "url"]
20
+ },
21
+ {
22
+ "fact": "job.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
+ "fact": "application.status",
42
+ "key": "company-role:{Company|slug}:{Role|slug}",
43
+ "value": "{Status}",
44
+ "fields": {
45
+ "num": "{#}",
46
+ "date": "{Date}",
47
+ "company": "{Company}",
48
+ "role": "{Role}",
49
+ "score": "{Score}",
50
+ "status": "{Status}",
51
+ "pdf": "{PDF}",
52
+ "report": "{Report}",
53
+ "notes": "{Notes}"
54
+ },
55
+ "tags": ["tracker", "application"]
56
+ }
57
+ ]
58
+ },
59
+ {
60
+ "name": "application-single-file",
61
+ "include": ["data/applications.md", "applications.md"],
62
+ "format": "markdown-table",
63
+ "records": [
64
+ {
65
+ "fact": "application.status",
66
+ "key": "company-role:{Company|slug}:{Role|slug}",
67
+ "value": "{Status}",
68
+ "fields": {
69
+ "num": "{#}",
70
+ "date": "{Date}",
71
+ "company": "{Company}",
72
+ "role": "{Role}",
73
+ "score": "{Score}",
74
+ "status": "{Status}",
75
+ "pdf": "{PDF}",
76
+ "report": "{Report}",
77
+ "notes": "{Notes}"
78
+ },
79
+ "tags": ["tracker", "application"]
80
+ }
81
+ ]
82
+ },
83
+ {
84
+ "name": "tracker-additions",
85
+ "include": ["batch/tracker-additions/*.tsv", "batch/tracker-additions/merged/*.tsv"],
86
+ "format": "tsv",
87
+ "header": false,
88
+ "columns": ["num", "date", "company", "role", "statusOrScore", "scoreOrStatus", "pdf", "report", "notes"],
89
+ "records": [
90
+ {
91
+ "fact": "tracker.addition",
92
+ "key": "company-role:{company|slug}:{role|slug}",
93
+ "value": "{source}",
94
+ "fields": ["num", "date", "company", "role", "statusOrScore", "scoreOrStatus", "pdf", "report", "notes"],
95
+ "tags": ["tracker", "tsv"]
96
+ }
97
+ ]
98
+ },
99
+ {
100
+ "name": "pipeline",
101
+ "include": ["data/pipeline.md"],
102
+ "format": "text",
103
+ "rules": [
104
+ {
105
+ "fact": "job.url",
106
+ "pattern": "^\\s*-\\s*\\[(?<state>[ xX])\\]\\s+(?<url>https?://\\S+)",
107
+ "key": "url:{url}",
108
+ "value": "{state}",
109
+ "fields": {
110
+ "url": "{url}",
111
+ "state": "{state}",
112
+ "pipeline": "{source}"
113
+ },
114
+ "tags": ["pipeline", "url"]
115
+ }
116
+ ]
117
+ },
118
+ {
119
+ "name": "scan-history",
120
+ "include": ["data/scan-history.tsv"],
121
+ "format": "tsv",
122
+ "records": [
123
+ {
124
+ "fact": "job.url",
125
+ "key": "url:{url}",
126
+ "value": "{url}",
127
+ "fields": ["date", "company", "role", "url", "ats"],
128
+ "tags": ["scan", "url"]
129
+ }
130
+ ]
131
+ },
132
+ {
133
+ "name": "preflight-candidates-object",
134
+ "include": ["batch/preflight-candidates.json"],
135
+ "format": "json",
136
+ "records": [
137
+ {
138
+ "fact": "candidate.ready",
139
+ "path": "$.candidates[]",
140
+ "key": "{companyRoleKey}",
141
+ "value": "{url}",
142
+ "fields": {
143
+ "id": "{id}",
144
+ "company": "{company}",
145
+ "role": "{role}",
146
+ "companyRoleKey": "{companyRoleKey}",
147
+ "url": "{url}",
148
+ "score": "{score}",
149
+ "gateStatus": "{gate.status}",
150
+ "locationStatus": "{location.status}"
151
+ },
152
+ "tags": ["candidate", "preflight"]
153
+ }
154
+ ]
155
+ },
156
+ {
157
+ "name": "preflight-candidates-array",
158
+ "include": ["batch/preflight-candidates.json"],
159
+ "format": "json",
160
+ "records": [
161
+ {
162
+ "fact": "candidate.ready",
163
+ "path": "$[]",
164
+ "key": "{companyRoleKey}",
165
+ "value": "{url}",
166
+ "fields": {
167
+ "id": "{id}",
168
+ "company": "{company}",
169
+ "role": "{role}",
170
+ "companyRoleKey": "{companyRoleKey}",
171
+ "url": "{url}",
172
+ "score": "{score}",
173
+ "gateStatus": "{gate.status}",
174
+ "locationStatus": "{location.status}"
175
+ },
176
+ "tags": ["candidate", "preflight"]
177
+ }
178
+ ]
179
+ },
180
+ {
181
+ "name": "ledger",
182
+ "include": [".jobforge-ledger/*.jsonl"],
183
+ "format": "jsonl",
184
+ "records": [
185
+ {
186
+ "fact": "ledger.event",
187
+ "key": "{key}",
188
+ "value": "{type}",
189
+ "fields": {
190
+ "type": "{type}",
191
+ "key": "{key}",
192
+ "status": "{data.status}",
193
+ "source": "{source}"
194
+ },
195
+ "tags": ["ledger"]
196
+ }
197
+ ]
198
+ }
199
+ ]
200
+ }
@@ -33,6 +33,19 @@
33
33
  "index:has": "job-forge index:has",
34
34
  "index:query": "job-forge index:query",
35
35
  "index:explain": "job-forge index:explain",
36
+ "facts:build": "job-forge facts:build",
37
+ "facts:status": "job-forge facts:status",
38
+ "facts:verify": "job-forge facts:verify",
39
+ "facts:check": "job-forge facts:check",
40
+ "facts:has": "job-forge facts:has",
41
+ "facts:query": "job-forge facts:query",
42
+ "facts:explain": "job-forge facts:explain",
43
+ "score:compute": "job-forge score:compute",
44
+ "score:verify": "job-forge score:verify",
45
+ "score:check": "job-forge score:check",
46
+ "score:gate": "job-forge score:gate",
47
+ "score:compare": "job-forge score:compare",
48
+ "score:explain": "job-forge score:explain",
36
49
  "canon:normalize": "job-forge canon:normalize",
37
50
  "canon:key": "job-forge canon:key",
38
51
  "canon:compare": "job-forge canon:compare",
@@ -69,6 +82,7 @@
69
82
  ".jobforge-ledger/",
70
83
  ".jobforge-cache/",
71
84
  ".jobforge-index.json",
85
+ ".jobforge-facts.json",
72
86
  ".jobforge-redacted/",
73
87
  "batch/preflight-candidates.json",
74
88
  "batch/preflight-plan.json",