job-forge 2.14.29 → 2.14.31
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 +10 -4
- package/AGENTS.md +10 -4
- package/CLAUDE.md +10 -4
- package/README.md +8 -4
- package/bin/create-job-forge.mjs +10 -0
- package/bin/job-forge.mjs +65 -0
- package/docs/ARCHITECTURE.md +7 -1
- package/docs/CUSTOMIZATION.md +8 -0
- package/docs/README.md +1 -1
- package/docs/SETUP.md +4 -0
- package/iso/instructions.md +10 -4
- package/lib/jobforge-facts.mjs +178 -0
- package/lib/jobforge-redact.mjs +25 -0
- package/package.json +14 -1
- package/scripts/facts.mjs +238 -0
- package/scripts/redact.mjs +180 -0
- package/templates/facts.json +200 -0
- package/templates/migrations.json +13 -0
- package/templates/redact.json +77 -0
- package/verify-pipeline.mjs +20 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import {
|
|
4
|
+
buildFacts,
|
|
5
|
+
checkFactRequirements,
|
|
6
|
+
factId,
|
|
7
|
+
hasFact,
|
|
8
|
+
loadFactsConfig,
|
|
9
|
+
parseJson,
|
|
10
|
+
queryFacts,
|
|
11
|
+
verifyFactSet,
|
|
12
|
+
} from '@razroo/iso-facts';
|
|
13
|
+
import {
|
|
14
|
+
jobForgeCompanyRoleKey,
|
|
15
|
+
jobForgeUrlKey,
|
|
16
|
+
legacyCompanyRoleKey,
|
|
17
|
+
legacyUrlKey,
|
|
18
|
+
} from './jobforge-canon.mjs';
|
|
19
|
+
|
|
20
|
+
export const FACTS_FILE = '.jobforge-facts.json';
|
|
21
|
+
export const FACTS_CONFIG_FILE = 'templates/facts.json';
|
|
22
|
+
|
|
23
|
+
export function resolveProjectDir(projectDir = process.env.JOB_FORGE_PROJECT || process.cwd()) {
|
|
24
|
+
return projectDir;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function jobForgeFactsPath(projectDir = resolveProjectDir()) {
|
|
28
|
+
return process.env.JOB_FORGE_FACTS || join(projectDir, FACTS_FILE);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function jobForgeFactsConfigPath(projectDir = resolveProjectDir()) {
|
|
32
|
+
return process.env.JOB_FORGE_FACTS_CONFIG || join(projectDir, FACTS_CONFIG_FILE);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function factsExist(projectDir = resolveProjectDir()) {
|
|
36
|
+
return existsSync(jobForgeFactsPath(projectDir));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function readJobForgeFactsConfig(projectDir = resolveProjectDir()) {
|
|
40
|
+
const path = jobForgeFactsConfigPath(projectDir);
|
|
41
|
+
return loadFactsConfig(parseJson(readFileSync(path, 'utf8'), path));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildJobForgeFacts(options = {}, projectDir = resolveProjectDir()) {
|
|
45
|
+
const config = readJobForgeFactsConfig(projectDir);
|
|
46
|
+
const factSet = canonicalizeJobForgeFacts(buildFacts(config, { root: projectDir }), projectDir);
|
|
47
|
+
const out = options.out || jobForgeFactsPath(projectDir);
|
|
48
|
+
if (options.write !== false) {
|
|
49
|
+
writeFileSync(out, `${JSON.stringify(factSet, null, 2)}\n`, 'utf8');
|
|
50
|
+
}
|
|
51
|
+
return { factSet, out };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function readJobForgeFacts(projectDir = resolveProjectDir()) {
|
|
55
|
+
const path = jobForgeFactsPath(projectDir);
|
|
56
|
+
return parseJson(readFileSync(path, 'utf8'), path);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function ensureJobForgeFacts(options = {}, projectDir = resolveProjectDir()) {
|
|
60
|
+
if (options.rebuild !== false || !factsExist(projectDir)) {
|
|
61
|
+
return buildJobForgeFacts({ out: options.out }, projectDir).factSet;
|
|
62
|
+
}
|
|
63
|
+
return readJobForgeFacts(projectDir);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function queryJobForgeFacts(query = {}, options = {}, projectDir = resolveProjectDir()) {
|
|
67
|
+
return queryFacts(ensureJobForgeFacts(options, projectDir), query);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function hasJobForgeFact(query = {}, options = {}, projectDir = resolveProjectDir()) {
|
|
71
|
+
return hasFact(ensureJobForgeFacts(options, projectDir), query);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function verifyJobForgeFacts(options = {}, projectDir = resolveProjectDir()) {
|
|
75
|
+
const factSet = options.factSet || ensureJobForgeFacts(options, projectDir);
|
|
76
|
+
return verifyFactSet(factSet);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function checkJobForgeFacts(options = {}, projectDir = resolveProjectDir()) {
|
|
80
|
+
const factSet = options.factSet || ensureJobForgeFacts(options, projectDir);
|
|
81
|
+
const config = readJobForgeFactsConfig(projectDir);
|
|
82
|
+
return checkFactRequirements(factSet, config.requirements || []);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function jobForgeFactsSummary(projectDir = resolveProjectDir()) {
|
|
86
|
+
if (!factsExist(projectDir)) {
|
|
87
|
+
return {
|
|
88
|
+
path: jobForgeFactsPath(projectDir),
|
|
89
|
+
config: jobForgeFactsConfigPath(projectDir),
|
|
90
|
+
exists: false,
|
|
91
|
+
facts: 0,
|
|
92
|
+
files: 0,
|
|
93
|
+
sources: 0,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const factSet = readJobForgeFacts(projectDir);
|
|
97
|
+
return {
|
|
98
|
+
path: jobForgeFactsPath(projectDir),
|
|
99
|
+
config: jobForgeFactsConfigPath(projectDir),
|
|
100
|
+
exists: true,
|
|
101
|
+
facts: factSet.stats?.facts || 0,
|
|
102
|
+
files: factSet.stats?.files || 0,
|
|
103
|
+
sources: factSet.stats?.sources || 0,
|
|
104
|
+
configHash: factSet.configHash,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function canonicalizeJobForgeFacts(factSet, projectDir) {
|
|
109
|
+
const facts = (factSet.facts || []).map((fact) => canonicalizeJobForgeFact(fact, projectDir));
|
|
110
|
+
facts.sort(compareFacts);
|
|
111
|
+
return {
|
|
112
|
+
...factSet,
|
|
113
|
+
facts,
|
|
114
|
+
stats: {
|
|
115
|
+
...(factSet.stats || {}),
|
|
116
|
+
facts: facts.length,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function canonicalizeJobForgeFact(fact, projectDir) {
|
|
122
|
+
const key = canonicalFactKey(fact, projectDir);
|
|
123
|
+
if (key === fact.key) return fact;
|
|
124
|
+
const updated = { ...fact, key };
|
|
125
|
+
return { ...updated, id: factId(updated) };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function canonicalFactKey(fact, projectDir) {
|
|
129
|
+
if (isCompanyRoleFact(fact)) {
|
|
130
|
+
const { company, role } = companyRoleFields(fact);
|
|
131
|
+
if (company && role) return safeCompanyRoleKey(company, role, projectDir);
|
|
132
|
+
}
|
|
133
|
+
if (isUrlFact(fact)) {
|
|
134
|
+
const url = fact.fields?.url;
|
|
135
|
+
if (url) return safeUrlKey(url, projectDir);
|
|
136
|
+
}
|
|
137
|
+
return fact.key;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isCompanyRoleFact(fact) {
|
|
141
|
+
return fact.key?.startsWith('company-role:') ||
|
|
142
|
+
fact.fact === 'application.status' ||
|
|
143
|
+
fact.fact === 'tracker.addition' ||
|
|
144
|
+
fact.fact === 'candidate.ready';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function companyRoleFields(fact) {
|
|
148
|
+
const fields = fact.fields || {};
|
|
149
|
+
return {
|
|
150
|
+
company: fields.company || fields.Company,
|
|
151
|
+
role: fields.role || fields.Role,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isUrlFact(fact) {
|
|
156
|
+
return fact.key?.startsWith('url:') || fact.fact === 'job.url';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function safeCompanyRoleKey(company, role, projectDir) {
|
|
160
|
+
try {
|
|
161
|
+
return jobForgeCompanyRoleKey(company, role, projectDir);
|
|
162
|
+
} catch {
|
|
163
|
+
return legacyCompanyRoleKey(company, role);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function safeUrlKey(url, projectDir) {
|
|
168
|
+
try {
|
|
169
|
+
return jobForgeUrlKey(url, projectDir);
|
|
170
|
+
} catch {
|
|
171
|
+
return legacyUrlKey(url);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function compareFacts(a, b) {
|
|
176
|
+
return `${a.fact}\0${a.key || ''}\0${a.value || ''}\0${a.source?.path || ''}\0${a.source?.line || ''}\0${a.id}`
|
|
177
|
+
.localeCompare(`${b.fact}\0${b.key || ''}\0${b.value || ''}\0${b.source?.path || ''}\0${b.source?.line || ''}\0${b.id}`);
|
|
178
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import {
|
|
4
|
+
loadRedactConfig,
|
|
5
|
+
parseJson,
|
|
6
|
+
} from '@razroo/iso-redact';
|
|
7
|
+
|
|
8
|
+
export const REDACT_CONFIG_FILE = 'templates/redact.json';
|
|
9
|
+
|
|
10
|
+
export function resolveProjectDir(projectDir = process.env.JOB_FORGE_PROJECT || process.cwd()) {
|
|
11
|
+
return projectDir;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function jobForgeRedactConfigPath(projectDir = resolveProjectDir()) {
|
|
15
|
+
return process.env.JOB_FORGE_REDACT_CONFIG || join(projectDir, REDACT_CONFIG_FILE);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function redactConfigExists(projectDir = resolveProjectDir()) {
|
|
19
|
+
return existsSync(jobForgeRedactConfigPath(projectDir));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function readJobForgeRedactConfig(projectDir = resolveProjectDir()) {
|
|
23
|
+
const path = jobForgeRedactConfigPath(projectDir);
|
|
24
|
+
return loadRedactConfig(parseJson(readFileSync(path, 'utf8'), path));
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-forge",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.31",
|
|
4
4
|
"description": "AI-powered job search pipeline built on opencode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -55,6 +55,13 @@
|
|
|
55
55
|
"index:has": "node bin/job-forge.mjs index:has",
|
|
56
56
|
"index:verify": "node bin/job-forge.mjs index:verify",
|
|
57
57
|
"index:explain": "node bin/job-forge.mjs index:explain",
|
|
58
|
+
"facts:build": "node bin/job-forge.mjs facts:build",
|
|
59
|
+
"facts:status": "node bin/job-forge.mjs facts:status",
|
|
60
|
+
"facts:verify": "node bin/job-forge.mjs facts:verify",
|
|
61
|
+
"facts:check": "node bin/job-forge.mjs facts:check",
|
|
62
|
+
"facts:has": "node bin/job-forge.mjs facts:has",
|
|
63
|
+
"facts:query": "node bin/job-forge.mjs facts:query",
|
|
64
|
+
"facts:explain": "node bin/job-forge.mjs facts:explain",
|
|
58
65
|
"canon:normalize": "node bin/job-forge.mjs canon:normalize",
|
|
59
66
|
"canon:key": "node bin/job-forge.mjs canon:key",
|
|
60
67
|
"canon:compare": "node bin/job-forge.mjs canon:compare",
|
|
@@ -65,6 +72,10 @@
|
|
|
65
72
|
"postflight:status": "node bin/job-forge.mjs postflight:status",
|
|
66
73
|
"postflight:check": "node bin/job-forge.mjs postflight:check",
|
|
67
74
|
"postflight:explain": "node bin/job-forge.mjs postflight:explain",
|
|
75
|
+
"redact:scan": "node bin/job-forge.mjs redact:scan",
|
|
76
|
+
"redact:verify": "node bin/job-forge.mjs redact:verify",
|
|
77
|
+
"redact:apply": "node bin/job-forge.mjs redact:apply",
|
|
78
|
+
"redact:explain": "node bin/job-forge.mjs redact:explain",
|
|
68
79
|
"migrate:plan": "node bin/job-forge.mjs migrate:plan",
|
|
69
80
|
"migrate:apply": "node bin/job-forge.mjs migrate:apply",
|
|
70
81
|
"migrate:check": "node bin/job-forge.mjs migrate:check",
|
|
@@ -140,6 +151,7 @@
|
|
|
140
151
|
"@razroo/iso-capabilities": "^0.1.0",
|
|
141
152
|
"@razroo/iso-context": "^0.1.0",
|
|
142
153
|
"@razroo/iso-contract": "^0.1.0",
|
|
154
|
+
"@razroo/iso-facts": "^0.1.0",
|
|
143
155
|
"@razroo/iso-guard": "^0.1.0",
|
|
144
156
|
"@razroo/iso-index": "^0.1.0",
|
|
145
157
|
"@razroo/iso-ledger": "^0.1.0",
|
|
@@ -147,6 +159,7 @@
|
|
|
147
159
|
"@razroo/iso-orchestrator": "^0.1.0",
|
|
148
160
|
"@razroo/iso-postflight": "^0.1.0",
|
|
149
161
|
"@razroo/iso-preflight": "^0.1.0",
|
|
162
|
+
"@razroo/iso-redact": "^0.1.0",
|
|
150
163
|
"@razroo/iso-trace": "^0.4.0",
|
|
151
164
|
"playwright": "^1.58.1"
|
|
152
165
|
},
|
|
@@ -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,180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import { dirname, isAbsolute, relative, resolve } from 'path';
|
|
5
|
+
import {
|
|
6
|
+
formatConfigSummary,
|
|
7
|
+
formatScanResult,
|
|
8
|
+
loadRedactConfig,
|
|
9
|
+
parseJson,
|
|
10
|
+
redactText,
|
|
11
|
+
scanSources,
|
|
12
|
+
} from '@razroo/iso-redact';
|
|
13
|
+
import { PROJECT_DIR } from '../tracker-lib.mjs';
|
|
14
|
+
import {
|
|
15
|
+
jobForgeRedactConfigPath,
|
|
16
|
+
readJobForgeRedactConfig,
|
|
17
|
+
} from '../lib/jobforge-redact.mjs';
|
|
18
|
+
|
|
19
|
+
const USAGE = `job-forge redact - deterministic local redaction for exports
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
job-forge redact:scan [--input <file> ...] [--stdin] [--config <file>] [--json]
|
|
23
|
+
job-forge redact:verify [--input <file> ...] [--stdin] [--config <file>] [--json]
|
|
24
|
+
job-forge redact:apply (--input <file> | --stdin) [--output <file>] [--config <file>] [--json]
|
|
25
|
+
job-forge redact:explain [--config <file>] [--json]
|
|
26
|
+
job-forge redact:path
|
|
27
|
+
|
|
28
|
+
Default policy is templates/redact.json. Findings never print matched values;
|
|
29
|
+
previews are redacted length markers. Use apply to write a sanitized copy
|
|
30
|
+
before exporting traces, prompts, reports, or fixture text.`;
|
|
31
|
+
|
|
32
|
+
const [cmd = 'help', ...rawArgs] = process.argv.slice(2);
|
|
33
|
+
const opts = parseArgs(rawArgs);
|
|
34
|
+
|
|
35
|
+
if (opts.help || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
36
|
+
console.log(USAGE);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
if (cmd === 'path') {
|
|
42
|
+
console.log(configPath(opts));
|
|
43
|
+
} else if (cmd === 'scan' || cmd === 'verify') {
|
|
44
|
+
scan(cmd, opts);
|
|
45
|
+
} else if (cmd === 'apply') {
|
|
46
|
+
apply(opts);
|
|
47
|
+
} else if (cmd === 'explain') {
|
|
48
|
+
explain(opts);
|
|
49
|
+
} else {
|
|
50
|
+
console.error(`unknown redact command "${cmd}"\n`);
|
|
51
|
+
console.error(USAGE);
|
|
52
|
+
process.exit(2);
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseArgs(args) {
|
|
60
|
+
const opts = {
|
|
61
|
+
inputs: [],
|
|
62
|
+
stdin: false,
|
|
63
|
+
json: false,
|
|
64
|
+
help: false,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < args.length; i++) {
|
|
68
|
+
const arg = args[i];
|
|
69
|
+
if (arg === '--json') {
|
|
70
|
+
opts.json = true;
|
|
71
|
+
} else if (arg === '--input' || arg === '-i') {
|
|
72
|
+
opts.inputs.push(valueAfter(args, ++i, arg));
|
|
73
|
+
} else if (arg.startsWith('--input=')) {
|
|
74
|
+
opts.inputs.push(arg.slice('--input='.length));
|
|
75
|
+
} else if (arg === '--stdin') {
|
|
76
|
+
opts.stdin = true;
|
|
77
|
+
} else if (arg === '--output' || arg === '-o') {
|
|
78
|
+
opts.output = valueAfter(args, ++i, arg);
|
|
79
|
+
} else if (arg.startsWith('--output=')) {
|
|
80
|
+
opts.output = arg.slice('--output='.length);
|
|
81
|
+
} else if (arg === '--config' || arg === '-c') {
|
|
82
|
+
opts.config = valueAfter(args, ++i, arg);
|
|
83
|
+
} else if (arg.startsWith('--config=')) {
|
|
84
|
+
opts.config = arg.slice('--config='.length);
|
|
85
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
86
|
+
opts.help = true;
|
|
87
|
+
} else if (arg.startsWith('--')) {
|
|
88
|
+
throw new Error(`unknown flag "${arg}"`);
|
|
89
|
+
} else {
|
|
90
|
+
opts.inputs.push(arg);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return opts;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function valueAfter(values, index, flag) {
|
|
98
|
+
const value = values[index];
|
|
99
|
+
if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function scan(mode, opts) {
|
|
104
|
+
const sources = readSources(opts);
|
|
105
|
+
if (sources.length === 0) throw new Error(`${mode} requires at least one --input or --stdin source`);
|
|
106
|
+
const result = scanSources(readConfig(opts), sources);
|
|
107
|
+
if (opts.json) {
|
|
108
|
+
console.log(JSON.stringify(result, null, 2));
|
|
109
|
+
} else {
|
|
110
|
+
console.log(formatScanResult(result, mode));
|
|
111
|
+
}
|
|
112
|
+
if (mode === 'verify' && !result.ok) process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function apply(opts) {
|
|
116
|
+
const sources = readSources(opts);
|
|
117
|
+
if (sources.length !== 1) throw new Error('apply requires exactly one --input or --stdin source');
|
|
118
|
+
const source = sources[0];
|
|
119
|
+
const result = redactText(readConfig(opts), source.text, { source: source.name });
|
|
120
|
+
if (opts.output) {
|
|
121
|
+
const output = resolveOutputPath(opts.output);
|
|
122
|
+
mkdirSync(dirname(output), { recursive: true });
|
|
123
|
+
writeFileSync(output, result.text, 'utf8');
|
|
124
|
+
if (opts.json) {
|
|
125
|
+
console.log(JSON.stringify({ ...result, text: undefined, output }, null, 2));
|
|
126
|
+
} else {
|
|
127
|
+
console.log(`iso-redact: wrote ${relativePath(output)} (${result.findings.length} finding(s) redacted)`);
|
|
128
|
+
}
|
|
129
|
+
} else if (opts.json) {
|
|
130
|
+
console.log(JSON.stringify(result, null, 2));
|
|
131
|
+
} else {
|
|
132
|
+
process.stdout.write(result.text);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function explain(opts) {
|
|
137
|
+
const config = readConfig(opts);
|
|
138
|
+
if (opts.json) {
|
|
139
|
+
console.log(JSON.stringify(config, null, 2));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
console.log(`config: ${relativePath(configPath(opts))}`);
|
|
143
|
+
console.log(formatConfigSummary(config));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function readSources(opts) {
|
|
147
|
+
const sources = opts.inputs.map((input) => {
|
|
148
|
+
const path = resolveInputPath(input);
|
|
149
|
+
return {
|
|
150
|
+
name: relativePath(path),
|
|
151
|
+
text: readFileSync(path, 'utf8'),
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
if (opts.stdin) sources.push({ name: '<stdin>', text: readFileSync(0, 'utf8') });
|
|
155
|
+
return sources;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function readConfig(opts) {
|
|
159
|
+
if (opts.config) {
|
|
160
|
+
const path = resolveInputPath(opts.config);
|
|
161
|
+
return loadRedactConfig(parseJson(readFileSync(path, 'utf8'), path));
|
|
162
|
+
}
|
|
163
|
+
return readJobForgeRedactConfig(PROJECT_DIR);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function configPath(opts) {
|
|
167
|
+
return opts.config ? resolveInputPath(opts.config) : jobForgeRedactConfigPath(PROJECT_DIR);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function resolveInputPath(path) {
|
|
171
|
+
return isAbsolute(path) ? path : resolve(PROJECT_DIR, path);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function resolveOutputPath(path) {
|
|
175
|
+
return isAbsolute(path) ? path : resolve(PROJECT_DIR, path);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function relativePath(path) {
|
|
179
|
+
return relative(PROJECT_DIR, path) || '.';
|
|
180
|
+
}
|