evaluation-cache.mjs
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env node
/** Local report-backed public facts cache (#1025); identity belongs to #1030. */
import { readFileSync, readdirSync, lstatSync, realpathSync, writeFileSync, chmodSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { randomUUID } from 'node:crypto';
import { parseArgs } from 'node:util';
import { JSON_SCHEMA } from 'js-yaml';
import { loadYaml } from './lib/yaml-document.mjs';
import { machineSummaryStart } from './lib/machine-summary-section.mjs';
import { getCareerOpsRoot } from './path-resolver.mjs';
import { acquireTrackerLock, trackerLockDirFor, renameSyncWithRetry } from './tracker-utils.mjs';
import { isMainModule } from './lib/is-main-module.mjs';
import { validateFlags } from './lib/cli-flags.mjs';
export const JOB_FACTS_TTL_MS = 24 * 60 * 60 * 1000;
const LIVENESS_TTL_MS = 5 * 60 * 1000;
const PUBLIC_FIELDS = ['company', 'role', 'advertised_comp', 'reports_to'];
const CACHE_FIELDS = ['schema_version', 'listing_fingerprint', 'captured_at', 'invalidated_at'];
const EXPLICIT_CLOSURE_CODES = new Set(['http_gone', 'expired_url', 'expired_body', 'listing_page']);
const miss = (reason) => ({ status: 'miss', reason });
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
// No permissive Date.parse fallback: date-only/local time and normalized invalid
// calendar dates must not accidentally authorize reuse.
function timestamp(value) {
if (typeof value !== 'string' || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{3})?Z$/.test(value)) return NaN;
const n = Date.parse(value);
return Number.isFinite(n) && new Date(n).toISOString() === (value.length === 20 ? value.replace(/Z$/, '.000Z') : value) ? n : NaN;
}
/**
* The report's only Machine Summary. Two candidate headings are a miss, not a
* choice, and the one heading must be the report's own (one pasted inside the
* archived JD is not).
*/
function machineSummary(markdown) {
if (typeof markdown !== 'string' || markdown.length > 2_000_000) return null;
const lines = markdown.split(/(?<=\n)/);
let fence = null, offset = 0;
const headings = [];
for (const line of lines) {
const mark = line.match(/^ {0,3}(`{3,}|~{3,})(.*)/);
if (fence) {
if (mark && mark[1][0] === fence[0] && mark[1].length >= fence.length && !mark[2].trim()) fence = null;
} else if (mark) fence = mark[1];
else if (/^##[ \t]+Machine Summary[ \t]*\r?\n?$/.test(line)) headings.push({ line: offset, end: offset + line.length });
offset += line.length;
}
if (headings.length !== 1 || headings[0].line !== machineSummaryStart(markdown)) return null;
const start = headings[0].end;
const match = markdown.slice(start).match(/^(?:[ \t]*\r?\n)*(`{3,})(?:yaml|yml|json)[ \t]*\r?\n([\s\S]*?)\r?\n\1[ \t]*(?=\r?\n|$)/);
if (!match) return null;
const raw = match[2];
const rawStart = start + match[0].indexOf(raw);
try {
const summary = loadYaml(raw, { schema: JSON_SCHEMA });
return isObject(summary) ? { summary, raw, rawStart } : null;
} catch { return null; }
}
/** Explicit projection only. Never serialize the report, header or private maps. */
function publicFacts(summary) {
const facts = {};
for (const field of PUBLIC_FIELDS) {
const value = summary[field] ?? null;
if (value !== null && (typeof value !== 'string' || !value.trim() || value.length > 2000)) return null;
facts[field] = value;
}
return facts.company && facts.role ? facts : null;
}
export function extractJobFacts(markdown) {
const parsed = machineSummary(markdown);
return parsed ? publicFacts(parsed.summary) : null;
}
async function identityModule() {
try { return await import('./listing-fingerprint.mjs'); }
catch (error) {
// This dependent draft can be installed before #1030. No substitute key,
// URL heuristic or local copy: reuse stays disabled until it is installed.
if (error.code === 'ERR_MODULE_NOT_FOUND' && error.url === new URL('./listing-fingerprint.mjs', import.meta.url).href) return null;
throw error;
}
}
function livenessValid(identity, fingerprint, liveness, now) {
if (!isObject(liveness) || !['active', 'expired', 'uncertain'].includes(liveness.result)) return false;
const checked = timestamp(liveness.checked_at);
if (!Number.isFinite(checked) || checked > now || now - checked >= LIVENESS_TTL_MS) return false;
try {
// Canonical host/path omit query, so they cannot prove identity (e.g.
// Greenhouse embed/job_app?token=...). Bind to the independently resolved
// ATS record of the posting actually checked, using the same #1030 API.
if (identity.listingKey(liveness.listing_fingerprint) !== identity.listingKey(fingerprint)) return false;
const context = identity.computeListingFingerprint({ url: liveness.url });
return !!fingerprint.canonical_host && !!fingerprint.canonical_path &&
context.canonical_host === liveness.listing_fingerprint.canonical_host &&
context.canonical_path === liveness.listing_fingerprint.canonical_path &&
context.canonical_host === fingerprint.canonical_host && context.canonical_path === fingerprint.canonical_path;
} catch { return false; }
}
async function contextFor({ fingerprint, liveness, now }) {
if (!Number.isFinite(now)) throw new TypeError('now must be a finite timestamp');
const identity = await identityModule();
if (!identity) return { failure: miss('listing-identity-unavailable') };
let key;
try { key = identity.listingKey(fingerprint); }
catch { return { failure: miss('invalid-identity') }; }
if (!key) return { failure: miss('partial-identity') };
if (!livenessValid(identity, fingerprint, liveness, now)) return { failure: miss('liveness-unverified') };
return { identity, key };
}
function reportFiles(reportsDir) {
try {
// A report symlink could read or rewrite unrelated user data. Only direct
// regular Markdown files in the selected reports directory are eligible.
if (lstatSync(reportsDir).isSymbolicLink()) throw new Error('reports directory must not be a symlink');
return readdirSync(reportsDir, { withFileTypes: true })
.filter(entry => entry.isFile() && entry.name.endsWith('.md'))
.map(entry => join(reportsDir, entry.name)).sort();
} catch (error) {
if (error.code === 'ENOENT') return [];
throw error;
}
}
function reportRecord(path, identity, key) {
try {
if (!lstatSync(path).isFile() || lstatSync(path).isSymbolicLink()) return null;
const markdown = readFileSync(path, 'utf8');
const parsed = machineSummary(markdown);
const meta = parsed?.summary.job_facts_cache;
if (!isObject(meta) || meta.schema_version !== 1 || Object.keys(meta).some(k => !CACHE_FIELDS.includes(k))) return null;
if (identity.listingKey(meta.listing_fingerprint) !== key) return null;
const captured = timestamp(meta.captured_at);
// An invalid invalidation marker must fail closed, never become 'not invalidated'.
const invalidated = meta.invalidated_at == null ? null : timestamp(meta.invalidated_at);
return { path, markdown, parsed, meta, captured, invalidated, facts: publicFacts(parsed.summary) };
} catch (error) {
if (error.code && error.code !== 'ENOENT') throw error;
return null;
}
}
/** Read-only lookup. Call invalidateJobFacts on a confirmed liveness miss. */
export async function lookupJobFacts({ reportsDir = join(getCareerOpsRoot(), 'reports'), fingerprint, liveness, now = Date.now() } = {}) {
const { identity, key, failure } = await contextFor({ fingerprint, liveness, now });
if (failure) return failure;
if (liveness.result !== 'active') return miss(`liveness-${liveness.result}`);
const records = reportFiles(reportsDir).map(path => reportRecord(path, identity, key)).filter(Boolean);
if (records.some(r => r.invalidated !== null && (!Number.isFinite(r.invalidated) || r.invalidated > now))) return miss('invalid-invalidation');
const invalidatedAt = Math.max(-Infinity, ...records.map(r => r.invalidated ?? -Infinity));
if (timestamp(liveness.checked_at) <= invalidatedAt) return miss('liveness-before-invalidation');
const fresh = records.filter(r => r.facts && Number.isFinite(r.captured) && r.captured <= now &&
now - r.captured < JOB_FACTS_TTL_MS && r.captured > invalidatedAt)
.sort((a, b) => b.captured - a.captured);
if (!fresh.length) return miss('no-fresh-facts');
const newest = fresh[0];
return { status: 'hit', payload: {
schema_version: 1, listing_key: key, captured_at: newest.meta.captured_at, job_facts: newest.facts,
} };
}
/** Persist tombstones in all matching reports; no separate cache store. */
export async function invalidateJobFacts({ reportsDir = join(getCareerOpsRoot(), 'reports'), fingerprint, liveness, now = Date.now() } = {}) {
const { identity, key, failure } = await contextFor({ fingerprint, liveness, now });
if (failure) return failure;
if (liveness.result !== 'expired' || !EXPLICIT_CLOSURE_CODES.has(liveness.code)) {
throw new TypeError('invalidation requires explicit closure evidence');
}
const files = reportFiles(reportsDir);
if (!files.length) return miss('no-cached-facts');
const lock = await acquireTrackerLock(trackerLockDirFor(join(realpathSync(reportsDir), '.job-facts-cache')), { timeoutMs: 5000 });
try {
const records = reportFiles(reportsDir).map(path => reportRecord(path, identity, key)).filter(Boolean);
if (!records.length) return miss('no-cached-facts');
const invalidatedAt = new Date(Math.max(now, ...records
.map(record => Number.isFinite(record.invalidated) ? record.invalidated : -Infinity))).toISOString();
// Stamp every duplicate so deleting/archiving one report cannot remove
// the only closure marker and revive another still-fresh copy.
for (const record of records) {
const meta = { schema_version: 1, listing_fingerprint: record.meta.listing_fingerprint,
captured_at: record.meta.captured_at, invalidated_at: invalidatedAt };
// Replace only this top-level cache block. The rest of the YAML and the
// report stay byte-identical, including private comments and formatting.
const block = /^job_facts_cache:[^\r\n]*(?:\r?\n(?:[ \t]+[^\r\n]*|[ \t]*))*/m;
const raw = record.parsed.raw;
const matching = raw.match(block);
if (!matching) throw new Error('cache metadata must use an unquoted top-level job_facts_cache key');
const replacement = `job_facts_cache: ${JSON.stringify(meta)}\n`;
const updated = raw.replace(block, () => replacement);
// Reject YAML aliases or unusual layout instead of risking a broader rewrite.
const reparsed = loadYaml(updated, { schema: JSON_SCHEMA });
const expected = { ...record.parsed.summary, job_facts_cache: meta };
if (JSON.stringify(reparsed) !== JSON.stringify(expected)) throw new Error('cache metadata cannot be updated safely');
const start = record.parsed.rawStart;
const temp = `${record.path}.${randomUUID()}.tmp`;
try {
const mode = lstatSync(record.path).mode & 0o777;
writeFileSync(temp, record.markdown.slice(0, start) + updated + record.markdown.slice(start + raw.length), { flag: 'wx', mode: 0o600 });
// Reports contain private fit: an atomic rewrite must not widen access.
chmodSync(temp, mode);
renameSyncWithRetry(temp, record.path);
} finally { rmSync(temp, { force: true }); }
}
return { status: 'invalidated', invalidated_at: invalidatedAt };
} finally { lock.release(); }
}
const USAGE = `Usage: node evaluation-cache.mjs --identity FILE --liveness FILE [--reports DIR] [--invalidate]
Read local reports and print strictly allowlisted public job facts as JSON.
--identity FILE #1030 listing-fingerprint record (JSON)
--liveness FILE Current {url, listing_fingerprint, result, checked_at} (JSON)
--reports DIR Defaults to reports/ under the resolved career-ops data root
--invalidate Record confirmed expired liveness in existing report metadata
--help, -h Show help
Only a strong identity, active check less than 5 minutes old, and facts less than
24 hours old authorize reuse. No network requests or candidate scoring. Lookup
is read-only; --invalidate updates only the report's job_facts_cache metadata.`;
async function main() {
const args = process.argv.slice(2);
validateFlags(args, ['--identity', '--liveness', '--reports', '--invalidate', '--help', '-h'], USAGE,
{ valueFlags: ['--identity', '--liveness', '--reports'], requireOperand: true });
const { values } = parseArgs({ args, options: {
identity: { type: 'string' }, liveness: { type: 'string' }, reports: { type: 'string' }, invalidate: { type: 'boolean' },
} });
if (!values.identity || !values.liveness) throw new TypeError('--identity and --liveness are required');
const options = { fingerprint: JSON.parse(readFileSync(values.identity, 'utf8')),
liveness: JSON.parse(readFileSync(values.liveness, 'utf8')), reportsDir: values.reports };
console.log(JSON.stringify(await (values.invalidate ? invalidateJobFacts(options) : lookupJobFacts(options)), null, 2));
}
if (isMainModule(import.meta.url)) main().catch(() => {
// Input/report parse errors can contain candidate text: do not echo them.
console.error('evaluation-cache: invalid input or unavailable report files; see --help');
process.exitCode = 1;
});