job-forge 2.14.33 → 2.14.35
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 -49
- package/AGENTS.md +11 -49
- package/CLAUDE.md +11 -49
- package/README.md +8 -4
- package/bin/create-job-forge.mjs +42 -0
- package/bin/job-forge.mjs +69 -0
- package/docs/ARCHITECTURE.md +18 -3
- package/docs/CUSTOMIZATION.md +8 -0
- package/docs/README.md +2 -2
- package/docs/SETUP.md +4 -0
- package/iso/instructions.md +11 -49
- package/lib/jobforge-lineage.mjs +122 -0
- package/lib/jobforge-prioritize.mjs +294 -0
- package/modes/README.md +2 -0
- package/modes/reference-local-helpers.md +55 -0
- package/package.json +19 -2
- package/scripts/check-helper-integration.mjs +143 -0
- package/scripts/check-iso-smoke.mjs +4 -2
- package/scripts/lineage.mjs +247 -0
- package/scripts/prioritize.mjs +323 -0
- package/templates/migrations.json +17 -0
- package/templates/prioritize.json +125 -0
- package/verify-pipeline.mjs +48 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { relative, resolve } from 'path';
|
|
4
|
+
import {
|
|
5
|
+
formatCheckResult,
|
|
6
|
+
formatExplainGraph,
|
|
7
|
+
formatRecordResult,
|
|
8
|
+
formatStaleResult,
|
|
9
|
+
formatVerifyResult,
|
|
10
|
+
parseJson,
|
|
11
|
+
} from '@razroo/iso-lineage';
|
|
12
|
+
import { PROJECT_DIR } from '../tracker-lib.mjs';
|
|
13
|
+
import {
|
|
14
|
+
checkJobForgeLineage,
|
|
15
|
+
jobForgeLineagePath,
|
|
16
|
+
jobForgeLineageSummary,
|
|
17
|
+
lineageExists,
|
|
18
|
+
normalizeJobForgeLineageArtifact,
|
|
19
|
+
readJobForgeLineage,
|
|
20
|
+
readJobForgeLineageOrEmpty,
|
|
21
|
+
recordJobForgeLineage,
|
|
22
|
+
staleJobForgeLineage,
|
|
23
|
+
verifyJobForgeLineage,
|
|
24
|
+
} from '../lib/jobforge-lineage.mjs';
|
|
25
|
+
|
|
26
|
+
const USAGE = `job-forge lineage - deterministic artifact lineage and stale-output checks
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
job-forge lineage:status [--json]
|
|
30
|
+
job-forge lineage:record --artifact <file> [--input <file>...] [--optional-input <file>...] [--kind <kind>] [--command <cmd>] [--metadata <json>] [--now <iso>] [--json]
|
|
31
|
+
job-forge lineage:check [--artifact <file>] [--json]
|
|
32
|
+
job-forge lineage:stale [--json]
|
|
33
|
+
job-forge lineage:verify [--json]
|
|
34
|
+
job-forge lineage:explain [--artifact <file>] [--json]
|
|
35
|
+
job-forge lineage:path
|
|
36
|
+
|
|
37
|
+
Default graph is .jobforge-lineage.json. Record generated reports, PDFs, and
|
|
38
|
+
other derived artifacts against their source CV/profile/report inputs, then
|
|
39
|
+
check whether outputs are stale after inputs change.`;
|
|
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(jobForgeLineagePath(PROJECT_DIR));
|
|
52
|
+
} else if (cmd === 'status') {
|
|
53
|
+
status(opts);
|
|
54
|
+
} else if (cmd === 'record') {
|
|
55
|
+
record(opts);
|
|
56
|
+
} else if (cmd === 'check') {
|
|
57
|
+
check(opts);
|
|
58
|
+
} else if (cmd === 'stale') {
|
|
59
|
+
stale(opts);
|
|
60
|
+
} else if (cmd === 'verify') {
|
|
61
|
+
verify(opts);
|
|
62
|
+
} else if (cmd === 'explain') {
|
|
63
|
+
explain(opts);
|
|
64
|
+
} else {
|
|
65
|
+
console.error(`unknown lineage command "${cmd}"\n`);
|
|
66
|
+
console.error(USAGE);
|
|
67
|
+
process.exit(2);
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseArgs(args) {
|
|
75
|
+
const opts = {
|
|
76
|
+
json: false,
|
|
77
|
+
help: false,
|
|
78
|
+
inputs: [],
|
|
79
|
+
optionalInputs: [],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < args.length; i++) {
|
|
83
|
+
const arg = args[i];
|
|
84
|
+
if (arg === '--json') {
|
|
85
|
+
opts.json = true;
|
|
86
|
+
} else if (arg === '--artifact') {
|
|
87
|
+
opts.artifact = valueAfter(args, ++i, '--artifact');
|
|
88
|
+
} else if (arg.startsWith('--artifact=')) {
|
|
89
|
+
opts.artifact = arg.slice('--artifact='.length);
|
|
90
|
+
} else if (arg === '--input') {
|
|
91
|
+
opts.inputs.push(valueAfter(args, ++i, '--input'));
|
|
92
|
+
} else if (arg.startsWith('--input=')) {
|
|
93
|
+
opts.inputs.push(arg.slice('--input='.length));
|
|
94
|
+
} else if (arg === '--optional-input') {
|
|
95
|
+
opts.optionalInputs.push(valueAfter(args, ++i, '--optional-input'));
|
|
96
|
+
} else if (arg.startsWith('--optional-input=')) {
|
|
97
|
+
opts.optionalInputs.push(arg.slice('--optional-input='.length));
|
|
98
|
+
} else if (arg === '--kind') {
|
|
99
|
+
opts.kind = valueAfter(args, ++i, '--kind');
|
|
100
|
+
} else if (arg.startsWith('--kind=')) {
|
|
101
|
+
opts.kind = arg.slice('--kind='.length);
|
|
102
|
+
} else if (arg === '--command') {
|
|
103
|
+
opts.command = valueAfter(args, ++i, '--command');
|
|
104
|
+
} else if (arg.startsWith('--command=')) {
|
|
105
|
+
opts.command = arg.slice('--command='.length);
|
|
106
|
+
} else if (arg === '--metadata') {
|
|
107
|
+
opts.metadata = parseMetadata(valueAfter(args, ++i, '--metadata'));
|
|
108
|
+
} else if (arg.startsWith('--metadata=')) {
|
|
109
|
+
opts.metadata = parseMetadata(arg.slice('--metadata='.length));
|
|
110
|
+
} else if (arg === '--now') {
|
|
111
|
+
opts.now = valueAfter(args, ++i, '--now');
|
|
112
|
+
} else if (arg.startsWith('--now=')) {
|
|
113
|
+
opts.now = arg.slice('--now='.length);
|
|
114
|
+
} else if (arg === '--out') {
|
|
115
|
+
opts.out = resolve(PROJECT_DIR, valueAfter(args, ++i, '--out'));
|
|
116
|
+
} else if (arg.startsWith('--out=')) {
|
|
117
|
+
opts.out = resolve(PROJECT_DIR, arg.slice('--out='.length));
|
|
118
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
119
|
+
opts.help = true;
|
|
120
|
+
} else {
|
|
121
|
+
throw new Error(`unknown flag "${arg}"`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return opts;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function valueAfter(values, index, flag) {
|
|
129
|
+
const value = values[index];
|
|
130
|
+
if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseMetadata(value) {
|
|
135
|
+
const parsed = parseJson(value, '--metadata');
|
|
136
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
137
|
+
throw new Error('--metadata must be a JSON object');
|
|
138
|
+
}
|
|
139
|
+
return parsed;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function status(opts) {
|
|
143
|
+
const summary = jobForgeLineageSummary(PROJECT_DIR);
|
|
144
|
+
if (opts.json) {
|
|
145
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!summary.exists) {
|
|
149
|
+
console.log(`lineage: missing (${relativePath(summary.path)})`);
|
|
150
|
+
console.log('run: job-forge lineage:record --artifact <file> --input <file>');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
console.log(`lineage: ${relativePath(summary.path)}`);
|
|
154
|
+
console.log(`records: ${summary.records}`);
|
|
155
|
+
console.log(`current: ${summary.current}`);
|
|
156
|
+
console.log(`stale: ${summary.stale}`);
|
|
157
|
+
console.log(`missing: ${summary.missing}`);
|
|
158
|
+
console.log(`check: ${summary.ok ? 'PASS' : 'STALE'}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function record(opts) {
|
|
162
|
+
const result = recordJobForgeLineage({
|
|
163
|
+
artifact: opts.artifact,
|
|
164
|
+
inputs: opts.inputs,
|
|
165
|
+
optionalInputs: opts.optionalInputs,
|
|
166
|
+
kind: opts.kind,
|
|
167
|
+
command: opts.command,
|
|
168
|
+
metadata: opts.metadata,
|
|
169
|
+
now: opts.now,
|
|
170
|
+
out: opts.out,
|
|
171
|
+
}, PROJECT_DIR);
|
|
172
|
+
if (opts.json) {
|
|
173
|
+
console.log(JSON.stringify(result, null, 2));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
console.log(formatRecordResult(result.graph, result.record, relativePath(result.out)));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function check(opts) {
|
|
180
|
+
if (!lineageExists(PROJECT_DIR)) {
|
|
181
|
+
if (opts.json) {
|
|
182
|
+
console.log(JSON.stringify({ ok: true, missing: true, path: jobForgeLineagePath(PROJECT_DIR) }, null, 2));
|
|
183
|
+
} else {
|
|
184
|
+
console.log(`lineage: missing (${relativePath(jobForgeLineagePath(PROJECT_DIR))})`);
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const result = checkJobForgeLineage({ artifact: opts.artifact }, PROJECT_DIR);
|
|
189
|
+
if (opts.json) {
|
|
190
|
+
console.log(JSON.stringify(result, null, 2));
|
|
191
|
+
} else {
|
|
192
|
+
console.log(formatCheckResult(result));
|
|
193
|
+
}
|
|
194
|
+
process.exit(result.ok ? 0 : 1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function stale(opts) {
|
|
198
|
+
if (!lineageExists(PROJECT_DIR)) {
|
|
199
|
+
if (opts.json) {
|
|
200
|
+
console.log(JSON.stringify({ ok: true, missing: true, path: jobForgeLineagePath(PROJECT_DIR) }, null, 2));
|
|
201
|
+
} else {
|
|
202
|
+
console.log(`lineage: missing (${relativePath(jobForgeLineagePath(PROJECT_DIR))})`);
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const result = staleJobForgeLineage({}, PROJECT_DIR);
|
|
207
|
+
if (opts.json) {
|
|
208
|
+
console.log(JSON.stringify(result, null, 2));
|
|
209
|
+
} else {
|
|
210
|
+
console.log(formatStaleResult(result));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function verify(opts) {
|
|
215
|
+
if (!lineageExists(PROJECT_DIR)) {
|
|
216
|
+
if (opts.json) {
|
|
217
|
+
console.log(JSON.stringify({ ok: true, missing: true, path: jobForgeLineagePath(PROJECT_DIR) }, null, 2));
|
|
218
|
+
} else {
|
|
219
|
+
console.log(`lineage: missing (${relativePath(jobForgeLineagePath(PROJECT_DIR))})`);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const result = verifyJobForgeLineage({}, PROJECT_DIR);
|
|
224
|
+
if (opts.json) {
|
|
225
|
+
console.log(JSON.stringify(result, null, 2));
|
|
226
|
+
} else {
|
|
227
|
+
console.log(formatVerifyResult(result));
|
|
228
|
+
}
|
|
229
|
+
process.exit(result.ok ? 0 : 1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function explain(opts) {
|
|
233
|
+
const graph = lineageExists(PROJECT_DIR) ? readJobForgeLineage(PROJECT_DIR) : readJobForgeLineageOrEmpty(PROJECT_DIR);
|
|
234
|
+
const artifact = opts.artifact
|
|
235
|
+
? normalizeJobForgeLineageArtifact(PROJECT_DIR, opts.artifact)
|
|
236
|
+
: undefined;
|
|
237
|
+
if (opts.json) {
|
|
238
|
+
const records = artifact ? graph.records.filter((record) => record.artifact.path === artifact) : graph.records;
|
|
239
|
+
console.log(JSON.stringify({ ...graph, records }, null, 2));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
console.log(formatExplainGraph(graph, artifact));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function relativePath(path) {
|
|
246
|
+
return relative(PROJECT_DIR, path) || '.';
|
|
247
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { isAbsolute, relative, resolve } from 'path';
|
|
5
|
+
import {
|
|
6
|
+
formatCheckResult,
|
|
7
|
+
formatConfigSummary,
|
|
8
|
+
formatPrioritizeResult,
|
|
9
|
+
formatVerifyResult,
|
|
10
|
+
loadPrioritizeItems,
|
|
11
|
+
parseJson,
|
|
12
|
+
} from '@razroo/iso-prioritize';
|
|
13
|
+
import { PROJECT_DIR } from '../tracker-lib.mjs';
|
|
14
|
+
import {
|
|
15
|
+
buildJobForgePrioritizeItems,
|
|
16
|
+
checkJobForgePrioritize,
|
|
17
|
+
jobForgePrioritizeConfigPath,
|
|
18
|
+
jobForgePrioritizeItemsPath,
|
|
19
|
+
jobForgePrioritizePath,
|
|
20
|
+
jobForgePrioritizeSummary,
|
|
21
|
+
prioritizeExists,
|
|
22
|
+
rankJobForgePrioritize,
|
|
23
|
+
readJobForgePrioritizeConfig,
|
|
24
|
+
selectJobForgePrioritize,
|
|
25
|
+
verifyJobForgePrioritize,
|
|
26
|
+
writeJobForgePrioritize,
|
|
27
|
+
writeJobForgePrioritizeItems,
|
|
28
|
+
} from '../lib/jobforge-prioritize.mjs';
|
|
29
|
+
|
|
30
|
+
const USAGE = `job-forge prioritize - deterministic next-action ranking
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
job-forge prioritize:status [--json]
|
|
34
|
+
job-forge prioritize:items [--now <iso>] [--out <file>] [--json]
|
|
35
|
+
job-forge prioritize:build [--now <iso>] [--profile <name>] [--limit N] [--out <file>] [--items-out <file>] [--json]
|
|
36
|
+
job-forge prioritize:rank [--items <file>] [--profile <name>] [--limit N] [--json]
|
|
37
|
+
job-forge prioritize:select [--items <file>] [--profile <name>] [--limit N] [--json]
|
|
38
|
+
job-forge prioritize:check [--items <file>] [--profile <name>] [--limit N] [--min-selected N] [--fail-on blocked|skipped,blocked|none] [--json]
|
|
39
|
+
job-forge prioritize:verify [--json]
|
|
40
|
+
job-forge prioritize:explain [--profile <name>] [--json]
|
|
41
|
+
job-forge prioritize:path [--config|--items]
|
|
42
|
+
|
|
43
|
+
Default policy is templates/prioritize.json. The generated queue is local
|
|
44
|
+
project state (.jobforge-prioritize.json), derived from source-backed facts and
|
|
45
|
+
due timeline items.`;
|
|
46
|
+
|
|
47
|
+
const [cmd = 'help', ...rawArgs] = process.argv.slice(2);
|
|
48
|
+
const opts = parseArgs(rawArgs);
|
|
49
|
+
|
|
50
|
+
if (opts.help || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
51
|
+
console.log(USAGE);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
if (cmd === 'path') {
|
|
57
|
+
path(opts);
|
|
58
|
+
} else if (cmd === 'status') {
|
|
59
|
+
status(opts);
|
|
60
|
+
} else if (cmd === 'items') {
|
|
61
|
+
items(opts);
|
|
62
|
+
} else if (cmd === 'build') {
|
|
63
|
+
build(opts);
|
|
64
|
+
} else if (cmd === 'rank') {
|
|
65
|
+
rank(opts);
|
|
66
|
+
} else if (cmd === 'select') {
|
|
67
|
+
select(opts);
|
|
68
|
+
} else if (cmd === 'check') {
|
|
69
|
+
check(opts);
|
|
70
|
+
} else if (cmd === 'verify') {
|
|
71
|
+
verify(opts);
|
|
72
|
+
} else if (cmd === 'explain') {
|
|
73
|
+
explain(opts);
|
|
74
|
+
} else {
|
|
75
|
+
console.error(`unknown prioritize command "${cmd}"\n`);
|
|
76
|
+
console.error(USAGE);
|
|
77
|
+
process.exit(2);
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseArgs(args) {
|
|
85
|
+
const opts = {
|
|
86
|
+
json: false,
|
|
87
|
+
help: false,
|
|
88
|
+
rebuild: true,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < args.length; i++) {
|
|
92
|
+
const arg = args[i];
|
|
93
|
+
if (arg === '--json') {
|
|
94
|
+
opts.json = true;
|
|
95
|
+
} else if (arg === '--no-rebuild') {
|
|
96
|
+
opts.rebuild = false;
|
|
97
|
+
} else if (arg === '--profile') {
|
|
98
|
+
opts.profile = valueAfter(args, ++i, '--profile');
|
|
99
|
+
} else if (arg.startsWith('--profile=')) {
|
|
100
|
+
opts.profile = arg.slice('--profile='.length);
|
|
101
|
+
} else if (arg === '--limit') {
|
|
102
|
+
opts.limit = parsePositiveInteger(valueAfter(args, ++i, '--limit'), '--limit');
|
|
103
|
+
} else if (arg.startsWith('--limit=')) {
|
|
104
|
+
opts.limit = parsePositiveInteger(arg.slice('--limit='.length), '--limit');
|
|
105
|
+
} else if (arg === '--min-selected') {
|
|
106
|
+
opts.minSelected = parseNonNegativeInteger(valueAfter(args, ++i, '--min-selected'), '--min-selected');
|
|
107
|
+
} else if (arg.startsWith('--min-selected=')) {
|
|
108
|
+
opts.minSelected = parseNonNegativeInteger(arg.slice('--min-selected='.length), '--min-selected');
|
|
109
|
+
} else if (arg === '--fail-on') {
|
|
110
|
+
opts.failOn = parseFailOn(valueAfter(args, ++i, '--fail-on'));
|
|
111
|
+
} else if (arg.startsWith('--fail-on=')) {
|
|
112
|
+
opts.failOn = parseFailOn(arg.slice('--fail-on='.length));
|
|
113
|
+
} else if (arg === '--now') {
|
|
114
|
+
opts.now = valueAfter(args, ++i, '--now');
|
|
115
|
+
} else if (arg.startsWith('--now=')) {
|
|
116
|
+
opts.now = arg.slice('--now='.length);
|
|
117
|
+
} else if (arg === '--items') {
|
|
118
|
+
if (!args[i + 1] || args[i + 1].startsWith('--')) {
|
|
119
|
+
opts.itemsPath = true;
|
|
120
|
+
} else {
|
|
121
|
+
opts.items = valueAfter(args, ++i, '--items');
|
|
122
|
+
}
|
|
123
|
+
} else if (arg.startsWith('--items=')) {
|
|
124
|
+
opts.items = arg.slice('--items='.length);
|
|
125
|
+
} else if (arg === '--out') {
|
|
126
|
+
opts.out = resolveInputPath(valueAfter(args, ++i, '--out'));
|
|
127
|
+
} else if (arg.startsWith('--out=')) {
|
|
128
|
+
opts.out = resolveInputPath(arg.slice('--out='.length));
|
|
129
|
+
} else if (arg === '--items-out') {
|
|
130
|
+
opts.itemsOut = resolveInputPath(valueAfter(args, ++i, '--items-out'));
|
|
131
|
+
} else if (arg.startsWith('--items-out=')) {
|
|
132
|
+
opts.itemsOut = resolveInputPath(arg.slice('--items-out='.length));
|
|
133
|
+
} else if (arg === '--config') {
|
|
134
|
+
opts.configPath = true;
|
|
135
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
136
|
+
opts.help = true;
|
|
137
|
+
} else {
|
|
138
|
+
throw new Error(`unknown flag "${arg}"`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return opts;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function valueAfter(values, index, flag) {
|
|
146
|
+
const value = values[index];
|
|
147
|
+
if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parsePositiveInteger(value, flag) {
|
|
152
|
+
const parsed = Number(value);
|
|
153
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${flag} must be a positive integer`);
|
|
154
|
+
return parsed;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseNonNegativeInteger(value, flag) {
|
|
158
|
+
const parsed = Number(value);
|
|
159
|
+
if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`${flag} must be a non-negative integer`);
|
|
160
|
+
return parsed;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseFailOn(value) {
|
|
164
|
+
if (value === 'none') return 'none';
|
|
165
|
+
return value.split(',').map((item) => item.trim()).filter(Boolean);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function path(opts) {
|
|
169
|
+
if (opts.configPath) {
|
|
170
|
+
console.log(jobForgePrioritizeConfigPath(PROJECT_DIR));
|
|
171
|
+
} else if (opts.itemsPath) {
|
|
172
|
+
console.log(jobForgePrioritizeItemsPath(PROJECT_DIR));
|
|
173
|
+
} else {
|
|
174
|
+
console.log(jobForgePrioritizePath(PROJECT_DIR));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function status(opts) {
|
|
179
|
+
const summary = jobForgePrioritizeSummary(PROJECT_DIR);
|
|
180
|
+
if (opts.json) {
|
|
181
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (!summary.exists) {
|
|
185
|
+
console.log(`prioritize: missing (${relativePath(summary.path)})`);
|
|
186
|
+
console.log('run: job-forge prioritize:build');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const result = verifyJobForgePrioritize({}, PROJECT_DIR);
|
|
190
|
+
console.log(`prioritize: ${relativePath(summary.path)}`);
|
|
191
|
+
console.log(`items: ${relativePath(summary.itemsPath)}`);
|
|
192
|
+
console.log(`profile: ${summary.profile}`);
|
|
193
|
+
console.log(`total: ${summary.items}`);
|
|
194
|
+
console.log(`selected: ${summary.selected}`);
|
|
195
|
+
console.log(`candidate: ${summary.candidate}`);
|
|
196
|
+
console.log(`skipped: ${summary.skipped}`);
|
|
197
|
+
console.log(`blocked: ${summary.blocked}`);
|
|
198
|
+
console.log(`verify: ${result.ok ? 'PASS' : 'FAIL'} (${result.errors} errors, ${result.warnings} warnings)`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function items(opts) {
|
|
202
|
+
const input = buildJobForgePrioritizeItems({ now: opts.now, rebuild: opts.rebuild }, PROJECT_DIR);
|
|
203
|
+
if (opts.out) writeJobForgePrioritizeItems(input, { out: opts.out }, PROJECT_DIR);
|
|
204
|
+
if (opts.json) {
|
|
205
|
+
console.log(JSON.stringify(input, null, 2));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (opts.out) console.log(`items: wrote ${relativePath(opts.out)}`);
|
|
209
|
+
console.log(`items: ${input.items.length}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function build(opts) {
|
|
213
|
+
const input = readItems(opts) || buildJobForgePrioritizeItems({ now: opts.now, rebuild: opts.rebuild }, PROJECT_DIR);
|
|
214
|
+
const result = rankJobForgePrioritize({
|
|
215
|
+
items: input,
|
|
216
|
+
profile: opts.profile,
|
|
217
|
+
limit: opts.limit,
|
|
218
|
+
}, PROJECT_DIR);
|
|
219
|
+
const itemsOut = writeJobForgePrioritizeItems(input, { out: opts.itemsOut }, PROJECT_DIR);
|
|
220
|
+
const out = writeJobForgePrioritize(result, { out: opts.out }, PROJECT_DIR);
|
|
221
|
+
if (opts.json) {
|
|
222
|
+
console.log(JSON.stringify({ out, itemsOut, result }, null, 2));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
console.log(`prioritize: wrote ${relativePath(out)}`);
|
|
226
|
+
console.log(`items: wrote ${relativePath(itemsOut)}`);
|
|
227
|
+
console.log(formatPrioritizeResult(result));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function rank(opts) {
|
|
231
|
+
const result = rankJobForgePrioritize({
|
|
232
|
+
items: readItems(opts),
|
|
233
|
+
profile: opts.profile,
|
|
234
|
+
limit: opts.limit,
|
|
235
|
+
now: opts.now,
|
|
236
|
+
rebuild: opts.rebuild,
|
|
237
|
+
}, PROJECT_DIR);
|
|
238
|
+
if (opts.out) writeJobForgePrioritize(result, { out: opts.out }, PROJECT_DIR);
|
|
239
|
+
if (opts.json) {
|
|
240
|
+
console.log(JSON.stringify(result, null, 2));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
console.log(formatPrioritizeResult(result));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function select(opts) {
|
|
247
|
+
const result = selectJobForgePrioritize({
|
|
248
|
+
items: readItems(opts),
|
|
249
|
+
profile: opts.profile,
|
|
250
|
+
limit: opts.limit,
|
|
251
|
+
now: opts.now,
|
|
252
|
+
rebuild: opts.rebuild,
|
|
253
|
+
}, PROJECT_DIR);
|
|
254
|
+
if (opts.out) writeJobForgePrioritize(result, { out: opts.out }, PROJECT_DIR);
|
|
255
|
+
if (opts.json) {
|
|
256
|
+
console.log(JSON.stringify(result, null, 2));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
console.log(formatPrioritizeResult(result));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function check(opts) {
|
|
263
|
+
const result = checkJobForgePrioritize({
|
|
264
|
+
items: readItems(opts),
|
|
265
|
+
profile: opts.profile,
|
|
266
|
+
limit: opts.limit,
|
|
267
|
+
minSelected: opts.minSelected,
|
|
268
|
+
failOn: opts.failOn,
|
|
269
|
+
now: opts.now,
|
|
270
|
+
rebuild: opts.rebuild,
|
|
271
|
+
}, PROJECT_DIR);
|
|
272
|
+
if (opts.json) {
|
|
273
|
+
console.log(JSON.stringify(result, null, 2));
|
|
274
|
+
} else {
|
|
275
|
+
console.log(formatCheckResult(result));
|
|
276
|
+
}
|
|
277
|
+
process.exit(result.ok ? 0 : 1);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function verify(opts) {
|
|
281
|
+
if (!prioritizeExists(PROJECT_DIR)) {
|
|
282
|
+
if (opts.json) {
|
|
283
|
+
console.log(JSON.stringify({ ok: true, missing: true, path: jobForgePrioritizePath(PROJECT_DIR) }, null, 2));
|
|
284
|
+
} else {
|
|
285
|
+
console.log(`prioritize: missing (${relativePath(jobForgePrioritizePath(PROJECT_DIR))})`);
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const result = verifyJobForgePrioritize({}, PROJECT_DIR);
|
|
290
|
+
if (opts.json) {
|
|
291
|
+
console.log(JSON.stringify(result, null, 2));
|
|
292
|
+
} else {
|
|
293
|
+
console.log(formatVerifyResult(result));
|
|
294
|
+
}
|
|
295
|
+
process.exit(result.ok ? 0 : 1);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function explain(opts) {
|
|
299
|
+
const config = readJobForgePrioritizeConfig(PROJECT_DIR);
|
|
300
|
+
if (opts.json) {
|
|
301
|
+
const value = opts.profile
|
|
302
|
+
? { ...config, profiles: config.profiles.filter((profile) => profile.name === opts.profile) }
|
|
303
|
+
: config;
|
|
304
|
+
console.log(JSON.stringify(value, null, 2));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
console.log(`config: ${relativePath(jobForgePrioritizeConfigPath(PROJECT_DIR))}`);
|
|
308
|
+
console.log(formatConfigSummary(config, opts.profile));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function readItems(opts) {
|
|
312
|
+
if (!opts.items) return undefined;
|
|
313
|
+
const path = resolveInputPath(opts.items);
|
|
314
|
+
return { items: loadPrioritizeItems(parseJson(readFileSync(path, 'utf8'), path)) };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function resolveInputPath(path) {
|
|
318
|
+
return isAbsolute(path) ? path : resolve(PROJECT_DIR, path);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function relativePath(path) {
|
|
322
|
+
return relative(PROJECT_DIR, path) || '.';
|
|
323
|
+
}
|
|
@@ -63,6 +63,20 @@
|
|
|
63
63
|
"timeline:check": "job-forge timeline:check",
|
|
64
64
|
"timeline:verify": "job-forge timeline:verify",
|
|
65
65
|
"timeline:explain": "job-forge timeline:explain",
|
|
66
|
+
"prioritize:status": "job-forge prioritize:status",
|
|
67
|
+
"prioritize:items": "job-forge prioritize:items",
|
|
68
|
+
"prioritize:build": "job-forge prioritize:build",
|
|
69
|
+
"prioritize:rank": "job-forge prioritize:rank",
|
|
70
|
+
"prioritize:select": "job-forge prioritize:select",
|
|
71
|
+
"prioritize:check": "job-forge prioritize:check",
|
|
72
|
+
"prioritize:verify": "job-forge prioritize:verify",
|
|
73
|
+
"prioritize:explain": "job-forge prioritize:explain",
|
|
74
|
+
"lineage:status": "job-forge lineage:status",
|
|
75
|
+
"lineage:record": "job-forge lineage:record",
|
|
76
|
+
"lineage:check": "job-forge lineage:check",
|
|
77
|
+
"lineage:stale": "job-forge lineage:stale",
|
|
78
|
+
"lineage:verify": "job-forge lineage:verify",
|
|
79
|
+
"lineage:explain": "job-forge lineage:explain",
|
|
66
80
|
"redact:scan": "job-forge redact:scan",
|
|
67
81
|
"redact:verify": "job-forge redact:verify",
|
|
68
82
|
"redact:apply": "job-forge redact:apply",
|
|
@@ -92,6 +106,9 @@
|
|
|
92
106
|
".jobforge-facts.json",
|
|
93
107
|
".jobforge-timeline.json",
|
|
94
108
|
".jobforge-timeline-events.jsonl",
|
|
109
|
+
".jobforge-prioritize.json",
|
|
110
|
+
".jobforge-prioritize-items.json",
|
|
111
|
+
".jobforge-lineage.json",
|
|
95
112
|
".jobforge-redacted/",
|
|
96
113
|
"data/timeline-events.jsonl",
|
|
97
114
|
"batch/preflight-candidates.json",
|