@gafj/gafj 0.1.1
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/README.md +99 -0
- package/bin/cli.js +628 -0
- package/bridge/byo_key.js +50 -0
- package/bridge/credits.js +72 -0
- package/bridge/paste.js +44 -0
- package/bridge/providers/anthropic.js +16 -0
- package/bridge/providers/gemini.js +15 -0
- package/bridge/providers/index.js +39 -0
- package/bridge/providers/openai_compatible.js +17 -0
- package/core/extract.js +55 -0
- package/core/generators/excerpt.js +64 -0
- package/core/generators/packet.js +197 -0
- package/core/generators/serialize.js +56 -0
- package/core/generators/slice.js +61 -0
- package/core/house_rules.js +49 -0
- package/core/kb_import.js +257 -0
- package/core/migrations/002-profile.sql +3 -0
- package/core/migrations/003-discover.sql +49 -0
- package/core/parsers.js +79 -0
- package/core/practice.js +75 -0
- package/core/provenance.js +90 -0
- package/core/qr.js +185 -0
- package/core/rules/categories.md +5 -0
- package/core/rules/deep_answers.md +7 -0
- package/core/rules/discover.md +30 -0
- package/core/rules/extract.md +31 -0
- package/core/rules/followup.md +9 -0
- package/core/rules/index.js +32 -0
- package/core/rules/onepager.md +15 -0
- package/core/rules/practice.md +10 -0
- package/core/rules/prep.md +47 -0
- package/core/rules/resume.md +49 -0
- package/core/rules/rubric.md +120 -0
- package/core/schema.sql +342 -0
- package/core/schemas/categories.js +16 -0
- package/core/schemas/cover.js +10 -0
- package/core/schemas/deep_answers.js +16 -0
- package/core/schemas/discover.js +19 -0
- package/core/schemas/extract.js +32 -0
- package/core/schemas/followup.js +10 -0
- package/core/schemas/index.js +32 -0
- package/core/schemas/lang.js +111 -0
- package/core/schemas/onepager.js +26 -0
- package/core/schemas/practice.js +18 -0
- package/core/schemas/prep.js +15 -0
- package/core/schemas/resume.js +25 -0
- package/core/scoring.js +586 -0
- package/core/text.js +98 -0
- package/core/tracker_map.js +94 -0
- package/core/ulid.js +38 -0
- package/core/validators/highlight_body_match.js +49 -0
- package/core/validators/index.js +43 -0
- package/core/validators/metric_pairing.js +52 -0
- package/core/validators/no_confabulated_history.js +63 -0
- package/core/validators/no_dashes.js +31 -0
- package/core/validators/no_derived_tenure.js +60 -0
- package/core/validators/style_config.js +18 -0
- package/core/validators/verb_ladder.js +51 -0
- package/core/verbs.js +43 -0
- package/http/api.js +260 -0
- package/http/guard.js +106 -0
- package/http/headers.js +17 -0
- package/http/hosted.js +273 -0
- package/http/identity.js +70 -0
- package/http/server.js +136 -0
- package/http/sse.js +41 -0
- package/mcp/server.js +25 -0
- package/mcp/tools.js +237 -0
- package/package.json +59 -0
- package/store/backup.js +63 -0
- package/store/backup_dir.js +60 -0
- package/store/config.js +32 -0
- package/store/context.js +74 -0
- package/store/db.js +37 -0
- package/store/discover.js +120 -0
- package/store/events.js +28 -0
- package/store/export_xlsx.js +62 -0
- package/store/home.js +36 -0
- package/store/import_kb.js +88 -0
- package/store/import_tracker.js +195 -0
- package/store/ingest.js +140 -0
- package/store/kb.js +202 -0
- package/store/log.js +88 -0
- package/store/migrate.js +55 -0
- package/store/onboarding.js +267 -0
- package/store/packets.js +110 -0
- package/store/practice.js +52 -0
- package/store/proposals.js +119 -0
- package/store/reads.js +110 -0
- package/store/render.js +111 -0
- package/store/reply.js +19 -0
- package/store/review.js +64 -0
- package/store/scoring.js +134 -0
- package/store/settings.js +142 -0
- package/store/snapshot.js +88 -0
- package/store/transitions.js +391 -0
- package/store/tx.js +71 -0
- package/store/tz.js +24 -0
- package/ui/app.css +137 -0
- package/ui/app.js +75 -0
- package/ui/fonts/plexmono-400.woff2 +0 -0
- package/ui/fonts/plexmono-500.woff2 +0 -0
- package/ui/fonts/plexmono-600.woff2 +0 -0
- package/ui/fonts/spacegrotesk-var.woff2 +0 -0
- package/ui/hosted/signin.css +8 -0
- package/ui/hosted/signin.html +30 -0
- package/ui/hosted/signin.js +28 -0
- package/ui/icon-192.png +0 -0
- package/ui/icon-512.png +0 -0
- package/ui/index.html +20 -0
- package/ui/lib.js +92 -0
- package/ui/manifest.webmanifest +18 -0
- package/ui/screens/application.js +73 -0
- package/ui/screens/dashboard.js +37 -0
- package/ui/screens/document.js +64 -0
- package/ui/screens/interview.js +85 -0
- package/ui/screens/kb.js +142 -0
- package/ui/screens/packet.js +122 -0
- package/ui/screens/postings.js +92 -0
- package/ui/screens/settings.js +115 -0
- package/ui/vendor/preact.mjs +1 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Provenance runs before every validator. A claim-bearing field must carry
|
|
5
|
+
* claims; every claim must resolve to the cited record's confirmed
|
|
6
|
+
* dimensions, or to this application's interviews, documents, and people.
|
|
7
|
+
* A cited record is a claim of provenance, not proof of truth: this checks
|
|
8
|
+
* the dimensions the engine knows how to check and nothing else.
|
|
9
|
+
*
|
|
10
|
+
* ctx: { slice: Map<id, record>, application: { interview_ids:Set, document_ids:Set, person_ids:Set, debriefs: Map<interview_id, text> } }
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { norm } = require("./text");
|
|
14
|
+
|
|
15
|
+
const YOURS = /\[YOURS\]/;
|
|
16
|
+
|
|
17
|
+
function nearby(text, slice, limit = 3) {
|
|
18
|
+
const { stems, overlap } = require("./text");
|
|
19
|
+
const s = stems(text);
|
|
20
|
+
return [...slice.values()]
|
|
21
|
+
.map((r) => ({ id: r.id, title: r.title, company: r.company, score: overlap(s, stems(r.title + " " + (r.summary || ""))) }))
|
|
22
|
+
.filter((x) => x.score > 0)
|
|
23
|
+
.sort((a, b) => b.score - a.score)
|
|
24
|
+
.slice(0, limit)
|
|
25
|
+
.map(({ id, title, company }) => ({ id, title, company }));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function confirmedMetric(record, value, outcomeKey) {
|
|
29
|
+
const { figureKey } = require("./text");
|
|
30
|
+
const target = norm(value).replace(/\s+/g, "");
|
|
31
|
+
const key = figureKey(value);
|
|
32
|
+
return (record.metrics_json || []).find((m) => m.review_state === "confirmed"
|
|
33
|
+
&& (norm(m.value).replace(/\s+/g, "") === target || (m.aliases || []).some((a) => norm(a).replace(/\s+/g, "") === target) || (m.keys || [m.key]).includes(key))
|
|
34
|
+
&& (!outcomeKey || m.outcome_key === outcomeKey));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function checkClaims(fields, ctx) {
|
|
38
|
+
const blocks = [];
|
|
39
|
+
const warnings = [];
|
|
40
|
+
const slice = ctx.slice || new Map();
|
|
41
|
+
const app = ctx.application || { interview_ids: new Set(), document_ids: new Set(), person_ids: new Set(), debriefs: new Map() };
|
|
42
|
+
for (const f of fields) {
|
|
43
|
+
if (!f.claimed) continue;
|
|
44
|
+
const text = f.value;
|
|
45
|
+
if (!text.trim() || YOURS.test(text) && text.replace(/\[YOURS\][^.]*\.?/g, "").trim().length < 20) continue;
|
|
46
|
+
if (!f.claims.length) {
|
|
47
|
+
blocks.push({ rule: "provenance", path: f.path, span: text.slice(0, 60), hint: "claim-bearing field has no claims; cite the record it draws on", nearby: nearby(text, slice) });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
for (const [i, c] of f.claims.entries()) {
|
|
51
|
+
const p = `${f.path.replace(/\.text$/, "")}.claims[${i}]`;
|
|
52
|
+
if (["record", "metric", "ownership", "tenure"].includes(c.kind)) {
|
|
53
|
+
const r = c.source_id && slice.get(c.source_id);
|
|
54
|
+
if (!r) { blocks.push({ rule: "provenance", path: p, span: String(c.source_id), hint: "unknown source_id; diagnostic neighbors only, never auto-substitute", nearby: nearby(text, slice) }); continue; }
|
|
55
|
+
if (c.kind === "metric") {
|
|
56
|
+
if (!c.value) { blocks.push({ rule: "provenance", path: p, span: "", hint: "metric claim needs a value" }); continue; }
|
|
57
|
+
const m = confirmedMetric(r, c.value, c.outcome_key);
|
|
58
|
+
if (!m) blocks.push({ rule: "provenance", path: p, span: c.value, hint: `${c.value} is not a confirmed metric on ${r.id}${c.outcome_key ? " for outcome " + c.outcome_key : ""}` });
|
|
59
|
+
} else if (c.kind === "ownership") {
|
|
60
|
+
if (!c.verb) blocks.push({ rule: "provenance", path: p, span: "", hint: "ownership claim needs a verb" });
|
|
61
|
+
if (!c.subject || !String(c.subject).trim()) blocks.push({ rule: "provenance", path: p, span: c.verb || "", hint: "ownership claim needs a subject" });
|
|
62
|
+
} else if (c.kind === "tenure") {
|
|
63
|
+
const ok = (r.verbatim_claims_json || []).some((v) => v.review_state === "confirmed" && ["tenure", "duration"].includes(v.type) && norm(v.text) === norm(c.value))
|
|
64
|
+
|| confirmedMetric(r, c.value);
|
|
65
|
+
if (!ok) blocks.push({ rule: "provenance", path: p, span: c.value || "", hint: `no confirmed tenure or duration claim "${c.value}" on ${r.id}` });
|
|
66
|
+
}
|
|
67
|
+
} else if (c.kind === "history") {
|
|
68
|
+
const ok = (c.interview_id && app.interview_ids.has(c.interview_id)) || (c.document_id && app.document_ids.has(c.document_id));
|
|
69
|
+
if (!ok) blocks.push({ rule: "provenance", path: p, span: c.interview_id || c.document_id || "", hint: "history claim must name an interview or document of this application" });
|
|
70
|
+
} else if (c.kind === "quote") {
|
|
71
|
+
const debrief = c.interview_id && app.debriefs.get(c.interview_id);
|
|
72
|
+
const ok = debrief && c.value && norm(debrief).includes(norm(c.value));
|
|
73
|
+
if (!ok) blocks.push({ rule: "provenance", path: p, span: c.value || "", hint: "a quote must be a verbatim substring of that interview's debrief" });
|
|
74
|
+
if (c.person_id && !app.person_ids.has(c.person_id)) blocks.push({ rule: "provenance", path: p, span: c.person_id, hint: "person is not on this application" });
|
|
75
|
+
} else {
|
|
76
|
+
blocks.push({ rule: "provenance", path: p, span: String(c.kind), hint: "unknown claim kind" });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { blocks, warnings };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Records a field cites, by id. */
|
|
84
|
+
function citedRecords(field, slice) {
|
|
85
|
+
const out = new Map();
|
|
86
|
+
for (const c of field.claims || []) if (c.source_id && slice.has(c.source_id)) out.set(c.source_id, slice.get(c.source_id));
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { checkClaims, citedRecords, confirmedMetric, nearby };
|
package/core/qr.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A small QR encoder for the launch link: byte mode, error correction L,
|
|
5
|
+
* versions 1 to 10 (up to 271 bytes). Pure, no dependencies. render()
|
|
6
|
+
* returns the code as terminal text using half-block characters so a
|
|
7
|
+
* phone camera can read it off the screen.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// capacity in data codewords for level L, versions 1..10, and the EC block layout [ecPerBlock, blocks1, data1, blocks2, data2]
|
|
11
|
+
const EC_L = {
|
|
12
|
+
1: [7, 1, 19, 0, 0], 2: [10, 1, 34, 0, 0], 3: [15, 1, 55, 0, 0], 4: [20, 1, 80, 0, 0], 5: [26, 1, 108, 0, 0],
|
|
13
|
+
6: [18, 2, 68, 0, 0], 7: [20, 2, 78, 0, 0], 8: [24, 2, 97, 0, 0], 9: [30, 2, 116, 0, 0], 10: [18, 2, 68, 2, 69],
|
|
14
|
+
};
|
|
15
|
+
const ALIGN = { 1: [], 2: [6, 18], 3: [6, 22], 4: [6, 26], 5: [6, 30], 6: [6, 34], 7: [6, 22, 38], 8: [6, 24, 42], 9: [6, 26, 46], 10: [6, 28, 50] };
|
|
16
|
+
|
|
17
|
+
// GF(256) tables
|
|
18
|
+
const EXP = new Array(512), LOG = new Array(256);
|
|
19
|
+
(() => { let x = 1; for (let i = 0; i < 255; i++) { EXP[i] = x; LOG[x] = i; x <<= 1; if (x & 0x100) x ^= 0x11d; } for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255]; })();
|
|
20
|
+
const mul = (a, b) => (a && b ? EXP[LOG[a] + LOG[b]] : 0);
|
|
21
|
+
|
|
22
|
+
function rsGenerator(n) {
|
|
23
|
+
let g = [1];
|
|
24
|
+
for (let i = 0; i < n; i++) {
|
|
25
|
+
const next = new Array(g.length + 1).fill(0);
|
|
26
|
+
for (let j = 0; j < g.length; j++) { next[j] ^= g[j]; next[j + 1] ^= mul(g[j], EXP[i]); }
|
|
27
|
+
g = next;
|
|
28
|
+
}
|
|
29
|
+
return g;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function rsEncode(data, n) {
|
|
33
|
+
const g = rsGenerator(n);
|
|
34
|
+
const out = data.concat(new Array(n).fill(0));
|
|
35
|
+
for (let i = 0; i < data.length; i++) {
|
|
36
|
+
const c = out[i];
|
|
37
|
+
if (!c) continue;
|
|
38
|
+
for (let j = 1; j < g.length; j++) out[i + j] ^= mul(g[j], c);
|
|
39
|
+
}
|
|
40
|
+
return out.slice(data.length);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function bitsFor(bytes, version) {
|
|
44
|
+
const [, b1, d1, b2, d2] = EC_L[version];
|
|
45
|
+
const total = b1 * d1 + b2 * d2;
|
|
46
|
+
const bits = [];
|
|
47
|
+
const push = (v, n) => { for (let i = n - 1; i >= 0; i--) bits.push((v >> i) & 1); };
|
|
48
|
+
push(0b0100, 4);
|
|
49
|
+
push(bytes.length, version >= 10 ? 16 : 8);
|
|
50
|
+
for (const b of bytes) push(b, 8);
|
|
51
|
+
push(0, Math.min(4, total * 8 - bits.length));
|
|
52
|
+
while (bits.length % 8) bits.push(0);
|
|
53
|
+
const codewords = [];
|
|
54
|
+
for (let i = 0; i < bits.length; i += 8) codewords.push(parseInt(bits.slice(i, i + 8).join(""), 2));
|
|
55
|
+
for (let pad = 0; codewords.length < total; pad ^= 1) codewords.push(pad ? 0x11 : 0xec);
|
|
56
|
+
return codewords;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function interleave(codewords, version) {
|
|
60
|
+
const [ec, b1, d1, b2, d2] = EC_L[version];
|
|
61
|
+
const blocks = [];
|
|
62
|
+
let p = 0;
|
|
63
|
+
for (let i = 0; i < b1; i++) { blocks.push(codewords.slice(p, p + d1)); p += d1; }
|
|
64
|
+
for (let i = 0; i < b2; i++) { blocks.push(codewords.slice(p, p + d2)); p += d2; }
|
|
65
|
+
const ecs = blocks.map((b) => rsEncode(b, ec));
|
|
66
|
+
const out = [];
|
|
67
|
+
const maxD = Math.max(d1, d2);
|
|
68
|
+
for (let i = 0; i < maxD; i++) for (const b of blocks) if (i < b.length) out.push(b[i]);
|
|
69
|
+
for (let i = 0; i < ec; i++) for (const e of ecs) out.push(e[i]);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function matrixFor(version) {
|
|
74
|
+
const n = version * 4 + 17;
|
|
75
|
+
const m = Array.from({ length: n }, () => new Array(n).fill(null)); // null = free
|
|
76
|
+
const set = (r, c, v) => { if (r >= 0 && r < n && c >= 0 && c < n) m[r][c] = v; };
|
|
77
|
+
const finder = (r, c) => { for (let i = -1; i <= 7; i++) for (let j = -1; j <= 7; j++) { const on = i >= 0 && i <= 6 && j >= 0 && j <= 6 && (i === 0 || i === 6 || j === 0 || j === 6 || (i >= 2 && i <= 4 && j >= 2 && j <= 4)); set(r + i, c + j, on ? 1 : 0); } };
|
|
78
|
+
finder(0, 0); finder(0, n - 7); finder(n - 7, 0);
|
|
79
|
+
const al = ALIGN[version];
|
|
80
|
+
for (const r of al) for (const c of al) {
|
|
81
|
+
if (m[r][c] !== null) continue;
|
|
82
|
+
for (let i = -2; i <= 2; i++) for (let j = -2; j <= 2; j++) set(r + i, c + j, Math.max(Math.abs(i), Math.abs(j)) === 1 ? 0 : 1);
|
|
83
|
+
}
|
|
84
|
+
for (let i = 8; i < n - 8; i++) { if (m[6][i] === null) m[6][i] = i % 2 === 0 ? 1 : 0; if (m[i][6] === null) m[i][6] = i % 2 === 0 ? 1 : 0; }
|
|
85
|
+
set(n - 8, 8, 1);
|
|
86
|
+
// reserve format areas
|
|
87
|
+
for (let i = 0; i < 9; i++) { if (m[8][i] === null) m[8][i] = 0; if (m[i][8] === null) m[i][8] = 0; }
|
|
88
|
+
for (let i = 0; i < 8; i++) { if (m[8][n - 1 - i] === null) m[8][n - 1 - i] = 0; if (m[n - 1 - i][8] === null) m[n - 1 - i][8] = 0; }
|
|
89
|
+
return m;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function place(m, data) {
|
|
93
|
+
const n = m.length;
|
|
94
|
+
const reserved = m.map((row) => row.map((v) => v !== null));
|
|
95
|
+
let bit = 0;
|
|
96
|
+
let up = true;
|
|
97
|
+
for (let c = n - 1; c > 0; c -= 2) {
|
|
98
|
+
if (c === 6) c--;
|
|
99
|
+
for (let k = 0; k < n; k++) {
|
|
100
|
+
const r = up ? n - 1 - k : k;
|
|
101
|
+
for (const cc of [c, c - 1]) {
|
|
102
|
+
if (reserved[r][cc]) continue;
|
|
103
|
+
const byte = data[bit >> 3];
|
|
104
|
+
m[r][cc] = byte === undefined ? 0 : (byte >> (7 - (bit & 7))) & 1;
|
|
105
|
+
bit++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
up = !up;
|
|
109
|
+
}
|
|
110
|
+
return reserved;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const MASKS = [(r, c) => (r + c) % 2 === 0, (r) => r % 2 === 0, (r, c) => c % 3 === 0, (r, c) => (r + c) % 3 === 0,
|
|
114
|
+
(r, c) => (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0, (r, c) => ((r * c) % 2) + ((r * c) % 3) === 0,
|
|
115
|
+
(r, c) => (((r * c) % 2) + ((r * c) % 3)) % 2 === 0, (r, c) => (((r + c) % 2) + ((r * c) % 3)) % 2 === 0];
|
|
116
|
+
|
|
117
|
+
function formatBits(mask) {
|
|
118
|
+
// level L = 01
|
|
119
|
+
let v = (0b01 << 3) | mask;
|
|
120
|
+
let rem = v << 10;
|
|
121
|
+
const g = 0b10100110111;
|
|
122
|
+
for (let i = 14; i >= 10; i--) if ((rem >> i) & 1) rem ^= g << (i - 10);
|
|
123
|
+
return ((v << 10) | rem) ^ 0b101010000010010;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function writeFormat(m, mask) {
|
|
127
|
+
const n = m.length;
|
|
128
|
+
const f = formatBits(mask);
|
|
129
|
+
const bit = (i) => (f >> i) & 1;
|
|
130
|
+
for (let i = 0; i < 6; i++) { m[8][i] = bit(14 - i); m[i][8] = bit(i); }
|
|
131
|
+
m[8][7] = bit(8); m[8][8] = bit(7); m[7][8] = bit(6);
|
|
132
|
+
for (let i = 0; i < 7; i++) m[n - 1 - i][8] = bit(14 - i);
|
|
133
|
+
for (let i = 0; i < 8; i++) m[8][n - 8 + i] = bit(7 - i);
|
|
134
|
+
m[n - 8][8] = 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function penalty(m) {
|
|
138
|
+
const n = m.length;
|
|
139
|
+
let score = 0;
|
|
140
|
+
for (let r = 0; r < n; r++) for (let c = 0, run = 1; c < n; c++) { if (c > 0 && m[r][c] === m[r][c - 1]) { run++; if (run === 5) score += 3; else if (run > 5) score++; } else run = 1; }
|
|
141
|
+
for (let c = 0; c < n; c++) for (let r = 0, run = 1; r < n; r++) { if (r > 0 && m[r][c] === m[r - 1][c]) { run++; if (run === 5) score += 3; else if (run > 5) score++; } else run = 1; }
|
|
142
|
+
for (let r = 0; r < n - 1; r++) for (let c = 0; c < n - 1; c++) if (m[r][c] === m[r][c + 1] && m[r][c] === m[r + 1][c] && m[r][c] === m[r + 1][c + 1]) score += 3;
|
|
143
|
+
let dark = 0;
|
|
144
|
+
for (const row of m) for (const v of row) dark += v;
|
|
145
|
+
score += Math.floor(Math.abs(dark * 100 / (n * n) - 50) / 5) * 10;
|
|
146
|
+
return score;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** @returns {number[][]} the module matrix (1 dark, 0 light) */
|
|
150
|
+
function encode(text) {
|
|
151
|
+
const bytes = [...Buffer.from(String(text), "utf8")];
|
|
152
|
+
let version = 1;
|
|
153
|
+
while (version <= 9) { const [, b1, d1, b2, d2] = EC_L[version]; if (b1 * d1 + b2 * d2 - 2 - (version >= 10 ? 1 : 0) >= bytes.length) break; version++; }
|
|
154
|
+
if (version > 9) throw new Error("text too long for a version 9 code");
|
|
155
|
+
const data = interleave(bitsFor(bytes, version), version);
|
|
156
|
+
let best = null;
|
|
157
|
+
for (let mask = 0; mask < 8; mask++) {
|
|
158
|
+
const m = matrixFor(version);
|
|
159
|
+
const reserved = place(m, data);
|
|
160
|
+
for (let r = 0; r < m.length; r++) for (let c = 0; c < m.length; c++) if (!reserved[r][c] && MASKS[mask](r, c)) m[r][c] ^= 1;
|
|
161
|
+
writeFormat(m, mask);
|
|
162
|
+
const p = penalty(m);
|
|
163
|
+
if (!best || p < best.p) best = { m, p };
|
|
164
|
+
}
|
|
165
|
+
return best.m;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Terminal rendering: two module rows per text line with half blocks, dark on light, a quiet zone of two. */
|
|
169
|
+
function render(text) {
|
|
170
|
+
const m = encode(text);
|
|
171
|
+
const n = m.length, q = 2;
|
|
172
|
+
const at = (r, c) => (r < 0 || c < 0 || r >= n || c >= n ? 0 : m[r][c]);
|
|
173
|
+
const lines = [];
|
|
174
|
+
for (let r = -q; r < n + q; r += 2) {
|
|
175
|
+
let line = "";
|
|
176
|
+
for (let c = -q; c < n + q; c++) {
|
|
177
|
+
const top = at(r, c), bottom = at(r + 1, c);
|
|
178
|
+
line += top && bottom ? "█" : top ? "▀" : bottom ? "▄" : " ";
|
|
179
|
+
}
|
|
180
|
+
lines.push(line);
|
|
181
|
+
}
|
|
182
|
+
return lines.join("\n");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { encode, render };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Scoring category proposal, engine version 1
|
|
2
|
+
|
|
3
|
+
Extract five to eight requirement categories from the POSTING in the posting's own language. Each carries verbatim evidence lines copied exactly from the posting text; the engine rejects any line that is not a verbatim substring. Weight 1 to 10 from the EMPLOYER's emphasis: repetition, position, required versus preferred. Never weight toward the candidate's strengths; a scorer that does is worthless. Flag hard gates: named credentials, years minimums, "must have" phrasing. Requirement blocks mode is mandatory; whole posting scoring degrades to term frequency.
|
|
4
|
+
|
|
5
|
+
Output only the JSON object matching OUTPUT SCHEMA. The candidate edits and freezes these categories before anything downstream uses them.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Deep answer rules, engine version 1
|
|
2
|
+
|
|
3
|
+
Full spoken word drafts, 60 to 120 seconds each, for the questions the prep marked as likely or deciding. Confirmed figures are written in as real, each with a metric claim. Everything only the candidate lived, deal names, stakeholders, the moment a deal nearly died, is the literal token [YOURS], shaped like the answer should sound. A placeholder is never delivered; the candidate fills it with truth or the answer is cut.
|
|
4
|
+
|
|
5
|
+
The lost deal and the proud deal are ALWAYS skeletons (skeleton_only true): interviewers probe two levels down and scripts die there.
|
|
6
|
+
|
|
7
|
+
Ownership verbs follow the ladder against each record's confirmed verbs. No tenure arithmetic. No reference to other companies in the search. Internal document: style rules warn, integrity rules block. Output only the JSON object matching OUTPUT SCHEMA.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Discovery rules, engine version 1
|
|
2
|
+
|
|
3
|
+
You are reading one candidate's confirmed KNOWLEDGE BASE SLICE (accomplishment records with ids, confirmed metrics, confirmed ownership verbs, claims, and wordings) and asking the questions a good career coach asks in the first hour: the ones that turn a thin record into a strong one. Produce JSON matching the OUTPUT SCHEMA. Nothing else. You are not writing anything for the candidate; you are asking.
|
|
4
|
+
|
|
5
|
+
## What a question is for
|
|
6
|
+
|
|
7
|
+
Every question exists to add one confirmable atom to one record: a figure the record lacks, the scope of an ownership claim, the situation that makes a result mean something, the story behind a bare bullet, a tenure phrase the candidate will stand behind, or a system or skill the work required that the record does not name. Tie each question to exactly one record by its id, copied exactly from the slice. A question about a record that is not in the slice is rejected whole.
|
|
8
|
+
|
|
9
|
+
## Where to dig
|
|
10
|
+
|
|
11
|
+
- HOLES first. The context lists the holes the engine already sees (no confirmed figure; an ownership verb with no scope). Ask about those before anything else, but ask better than the engine can: name what the figure probably is ("How many loads a day did that dispatch desk run?"), not "what number moved?".
|
|
12
|
+
- THEN the strongest records. A record with a figure and a verb still hides its situation and its stakes. Ask what was broken before, what the constraint was, who else was involved, what happened after.
|
|
13
|
+
- THEN the pattern across records. Dates that add up, a capability that shows up under three employers with no name, a system named in a wording and nowhere else.
|
|
14
|
+
- Skip records whose prior questions in the context were answered or dismissed unless you have a genuinely different question.
|
|
15
|
+
|
|
16
|
+
## How to ask
|
|
17
|
+
|
|
18
|
+
- One thing per question. A compound question gets a compound, useless answer.
|
|
19
|
+
- Ask for the atom in the form the record stores: a number with its unit, a headcount, a dollar figure, a span of time, a named system, a sentence the candidate would put on a resume.
|
|
20
|
+
- Say why in one clause: which posting language or which reviewer doubt the answer would settle.
|
|
21
|
+
- Never suggest an answer, a figure, or a phrase the candidate might accept without having lived it. "Roughly how many people" is a question; "about 12 people, right?" is a plant.
|
|
22
|
+
- Plain language. The candidate may not be technical. No jargon from this document.
|
|
23
|
+
|
|
24
|
+
## Do not
|
|
25
|
+
|
|
26
|
+
Do not propose records, rewrite wordings, compute tenure from dates, or infer a figure. Do not ask about anything outside the candidate's own work. Do not ask the same question twice in one reply. Twelve questions at most; six good ones beat twelve thin ones.
|
|
27
|
+
|
|
28
|
+
## Output
|
|
29
|
+
|
|
30
|
+
Only the JSON object matching OUTPUT SCHEMA. "notes" may say in one or two sentences what the records are strongest on and where they are thinnest.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Onboarding extraction rules, engine version 1
|
|
2
|
+
|
|
3
|
+
You are reading one SOURCE document (a resume or a similar record of work) for one candidate and proposing accomplishment records from it. Produce JSON matching the OUTPUT SCHEMA. Nothing else. Nothing you propose is trusted until the candidate confirms it, dimension by dimension, so precision beats coverage.
|
|
4
|
+
|
|
5
|
+
## Excerpt or nothing
|
|
6
|
+
|
|
7
|
+
Every factual atom you propose (company, role, date range, each metric value, each ownership verb, the scope line, each wording) carries the exact value and the character span [start, end) in the SOURCE text where that value appears. The value must equal the source text at that span, character for character. A metric with no span is rejected. A company the source never names is rejected. If you cannot point at it, leave it out. The summary is the only field that may paraphrase.
|
|
8
|
+
|
|
9
|
+
## One record per result
|
|
10
|
+
|
|
11
|
+
A record is one result the candidate produced: a figure moved, a thing built, a team led, a program delivered. Split a bullet that carries two results. Merge duplicate statements of one result only when they sit in the same source; across sources the engine suggests merges and the candidate decides.
|
|
12
|
+
|
|
13
|
+
## Metrics
|
|
14
|
+
|
|
15
|
+
A metric is a figure with its unit as written: "$430,000", "18%", "12 person team", "9 months". Copy the source form. Pair each with the outcome it measures in a few words. Never compute, convert, round, or infer a figure. "Nine years" stays "nine years".
|
|
16
|
+
|
|
17
|
+
## Verbs
|
|
18
|
+
|
|
19
|
+
The ownership verb is the lead verb of the wording, in the form the source uses. Do not upgrade "supported" to "led". Do not add a verb the source does not use.
|
|
20
|
+
|
|
21
|
+
## Wordings
|
|
22
|
+
|
|
23
|
+
Every bullet or sentence that states the result, verbatim, each with its span. The first wording is the one you would recommend. Keep the source's punctuation.
|
|
24
|
+
|
|
25
|
+
## Style observed
|
|
26
|
+
|
|
27
|
+
Report what the source does: dashes (none, hyphens, em dashes, mixed), how it spells ecommerce, whether percent is a symbol or a word. Observations only; the candidate chooses their house rules.
|
|
28
|
+
|
|
29
|
+
## Do not
|
|
30
|
+
|
|
31
|
+
Do not invent tags the source does not support. Do not propose employment history the source does not contain. Do not read a job posting or any other text in this packet as the candidate's own experience; only the SOURCE is theirs.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Follow up rules, engine version 1
|
|
2
|
+
|
|
3
|
+
Thank you notes, nudges, and negotiation notes. You are given the application, the round just completed with its debrief, the people, and the candidate's PROFILE. Produce JSON matching the OUTPUT SCHEMA: kind, subject, body, and the open loop you closed.
|
|
4
|
+
|
|
5
|
+
- Thank you: sent the day after. Reference what the INTERVIEWER chose to engage on; that specificity is what makes it land, and it must come from the debrief as a verbatim quote claim or as a history claim on that round. Register matched to the reader. One substantive hook maximum. Any open loop from the interview (a skipped test, an honest caveat) gets closed with an offer, not an apology.
|
|
6
|
+
- Nudge: blame infrastructure, never people ("in case it is sitting in a queue"). Confirm understood logistics explicitly with the time zone, so mismatches surface early.
|
|
7
|
+
- Negotiation: never name the floor. Turn comp into a question about their numbers.
|
|
8
|
+
|
|
9
|
+
Outbound document: no dashes in the subject or the body, house vocabulary, blocks by default. The candidate's own voice wins over polish; propose, do not lecture. No figure without a metric claim. Nothing about other companies in the search. The app never sends; the candidate copies the passed draft into their mail client and marks it sent.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Engine rule assets, versioned. Candidate data never lives here; the packet
|
|
5
|
+
* builder supplies PROFILE from the store. A test greps these files for
|
|
6
|
+
* phone numbers and email addresses, and the private gate greps them for the
|
|
7
|
+
* candidate's own names.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const crypto = require("crypto");
|
|
13
|
+
|
|
14
|
+
const FILES = { resume: "resume.md", cover: "resume.md", prep: "prep.md", onepager: "onepager.md", deep_answers: "deep_answers.md",
|
|
15
|
+
followup: "followup.md", practice: "practice.md", categories: "categories.md", extract: "extract.md", discover: "discover.md", rubric: "rubric.md" };
|
|
16
|
+
|
|
17
|
+
const cache = new Map();
|
|
18
|
+
|
|
19
|
+
function rulesFor(op) {
|
|
20
|
+
const file = FILES[op];
|
|
21
|
+
if (!file) throw new Error(`no rules for operation ${op}`);
|
|
22
|
+
if (!cache.has(file)) cache.set(file, fs.readFileSync(path.join(__dirname, file), "utf8"));
|
|
23
|
+
return cache.get(file);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function rulesVersion() {
|
|
27
|
+
const h = crypto.createHash("sha256");
|
|
28
|
+
for (const f of [...new Set(Object.values(FILES))].sort()) h.update(fs.readFileSync(path.join(__dirname, f)));
|
|
29
|
+
return "1-" + h.digest("hex").slice(0, 8);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { rulesFor, rulesVersion, FILES };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# One-pager rules, engine version 1
|
|
2
|
+
|
|
3
|
+
A single printed page for the 90 seconds before the call and glances during it. You are given the same context as the prep for this round, plus the passed prep if one exists. Produce JSON matching the OUTPUT SCHEMA. The renderer adds the round, the people, and the datetime from the store; you never write a date.
|
|
4
|
+
|
|
5
|
+
Layout, top to bottom, and the content budget that makes it one page:
|
|
6
|
+
|
|
7
|
+
1. Posture line, one sentence of register. A hard stop line only when the schedule demands one.
|
|
8
|
+
2. Stories: three to five, one line each, a bold metric or capability lead plus the connection to this room. Every figure is a metric claim.
|
|
9
|
+
3. Amber gaps: one to four exact quoted phrasings that open the door before it is forced.
|
|
10
|
+
4. Answer keys: trigger phrases, never scripts. A script read aloud sounds read.
|
|
11
|
+
5. Questions to ask: three to six, pre picked, numbered by the renderer.
|
|
12
|
+
6. Do nots: two to five, cold and specific.
|
|
13
|
+
7. Technical page, only when the round includes a live test: exact formulas as identifiers, click paths written stepwise, a rules box for the trap taxonomy found in drilling, an error recovery script, and the closing line that bridges the test to the candidate's real work.
|
|
14
|
+
|
|
15
|
+
The budget is enforced by the schema: item counts and character caps. If a story does not fit its cap, cut a clause, never the figure's outcome. One page is a content budget, not a measurement. Outbound style rules apply: no dashes, house vocabulary. Ruled note lines fill the rest of the page; the sheet written on during the call becomes the debrief.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Practice environment rules, engine version 1
|
|
2
|
+
|
|
3
|
+
A drill workbook spec for a live test. You are given the round, the test type, and the interviewer's domain. Produce JSON matching the OUTPUT SCHEMA; a companion builder turns it into a workbook.
|
|
4
|
+
|
|
5
|
+
- Realistic scenario data in the interviewer's domain.
|
|
6
|
+
- Planted anomalies at seeded positions, machine verifiable: overbills, duplicates, orphan keys, dirty whitespace including a non breaking space that survives TRIM, text stored numbers, mislabeled categories, and one subtle reverse direction anomaly (an underbill among overbills) to teach two sided auditing.
|
|
7
|
+
- At least one planted lesson where the naive answer is wrong (the wrong tier row that looks like an overbill), because separating data errors from billing errors is the actual skill.
|
|
8
|
+
- A timed task list, escalating difficulty. An answer key with formulas as text. A lesson line.
|
|
9
|
+
|
|
10
|
+
No candidate claims live in this document; it carries no provenance. The manifest stays on the document so a later grader can check a drilled workbook against it.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Interview prep rules, engine version 1
|
|
2
|
+
|
|
3
|
+
You are preparing one candidate for one interview round. You are given this APPLICATION's facts only: the posting, its frozen scoring, the round (label, format, people with any intel), the debriefs of prior rounds of this same application, the documents already built for it, the candidate's PROFILE, and a KNOWLEDGE BASE SLICE of confirmed records with ids. Produce JSON matching the OUTPUT SCHEMA. Nothing else.
|
|
4
|
+
|
|
5
|
+
## The one thing
|
|
6
|
+
|
|
7
|
+
Open with the single strategic frame for this room: the one thing to understand about this interview. If the interviewer's own path mirrors the candidate's, say so; mirrored arcs build rooms. Everything below should quietly land on that point.
|
|
8
|
+
|
|
9
|
+
## Posture (required)
|
|
10
|
+
|
|
11
|
+
"posture" is one line of register for the room: hunter, careful analyst, builder, operator. When another interview sits within a few hours of this one, the register note says how to switch and what not to carry over. The engine flags back to back rounds on the dashboard; your job is the head switch protocol.
|
|
12
|
+
|
|
13
|
+
## Core stories
|
|
14
|
+
|
|
15
|
+
Three to five, each with exact confirmed figures and the reason THIS interviewer cares. Every figure is a metric claim on its record. Every ownership verb is an ownership claim. A story that would need a number the records do not confirm is told without the number, or not told.
|
|
16
|
+
|
|
17
|
+
## The deciding question
|
|
18
|
+
|
|
19
|
+
The question that decides this interview, with a drafted answer in the candidate's voice. Two honest reasons beat one polished one. Do not oversell.
|
|
20
|
+
|
|
21
|
+
## Likely questions
|
|
22
|
+
|
|
23
|
+
Four to twelve, weighted toward the heaviest scoring categories and every genuine gap, each with a 90 second answer frame. Depth on request, not by default.
|
|
24
|
+
|
|
25
|
+
## Gaps in the open
|
|
26
|
+
|
|
27
|
+
The exact phrasings that raise each gap before the interviewer does, on the candidate's terms, each pairing the limitation with the nearest true strength. Never claim a tool, credential, or tenure the records do not confirm. Name the nearest real equivalent and say the specific thing would be new.
|
|
28
|
+
|
|
29
|
+
## Questions to ask
|
|
30
|
+
|
|
31
|
+
Six to eight, derived from the posting's stated problems. One relationship builder, asked second to last. One that quietly answers the interviewer's likely worry from the other direction.
|
|
32
|
+
|
|
33
|
+
## Do nots
|
|
34
|
+
|
|
35
|
+
Cold and specific: no salary number is ever named; no answer runs past two minutes without a check in; the phrasing bans PROFILE lists; the retired phrasings PROFILE lists.
|
|
36
|
+
|
|
37
|
+
## Campaign history
|
|
38
|
+
|
|
39
|
+
Any reference to a prior round, to what an interviewer said, or to homework completed must carry a history claim naming this application's interview or document, or a quote claim whose value is verbatim from that round's debrief. Do not mention other companies in this candidate's search; they are not in your context on purpose. Attributing words to a person without a verbatim quote claim is a rejection.
|
|
40
|
+
|
|
41
|
+
## Style
|
|
42
|
+
|
|
43
|
+
Prep is an internal document: dashes and vocabulary produce warnings, not blocks. Write in the candidate's voice. Placeholders for lived detail are the literal token [YOURS] and are never delivered.
|
|
44
|
+
|
|
45
|
+
## Output
|
|
46
|
+
|
|
47
|
+
Only the JSON object matching OUTPUT SCHEMA. Every claim bearing field carries claims with ids copied exactly from the slice. There is no date field in the output.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Resume rules, engine version 1
|
|
2
|
+
|
|
3
|
+
You are a resume tailoring engine for one candidate. You are given the candidate's PROFILE (contact header, employment skeleton, framing notes), a KNOWLEDGE BASE SLICE of verified accomplishment records (each with an id, company, role, dates, confirmed metrics with outcome keys, confirmed ownership verbs, verbatim claims, and wordings, strongest first), a target POSTING, and its frozen SCORING. Produce the strongest honest resume for that posting as JSON matching the OUTPUT SCHEMA. Nothing else.
|
|
4
|
+
|
|
5
|
+
## Why the architecture below exists
|
|
6
|
+
|
|
7
|
+
A fully tailored resume was rejected by a human screener in about 60 seconds: right role, real match, wrong document. The diagnosis was scan order. A long summary paragraph nobody reads, then a first entry a screener misread as "not working now", then two titles that read as the wrong function. All the evidence sat in bullets, which are read after titles, and the decision was already made. The rules below put the case in front of the decision instead of behind it. Follow them for that reason, not as a checklist.
|
|
8
|
+
|
|
9
|
+
## Employment history (critical)
|
|
10
|
+
|
|
11
|
+
The experience section MUST contain exactly the entries in PROFILE.skeleton, with exactly those company names, role titles, and dates, copied verbatim. Do not add, drop, merge, split, or rename entries. Reordering is permitted only under RELEVANCE ORDER. Entries marked concurrent_only render in the concurrent section or not at all. Entries marked ai_entry follow PROFILE.ai_entry_rule. Every listed role carries at least one bullet; trim depth, never leave a role empty. Bullets from a record whose role is a combined or aliased line belong to the skeleton entry PROFILE names for it.
|
|
12
|
+
|
|
13
|
+
## Document architecture
|
|
14
|
+
|
|
15
|
+
- HEADER. There is NO summary paragraph. The header is a four line stack: the candidate's name, then "headline" (the target role title, mirroring the posting's own title as closely as honesty allows), then "specialties" (exactly three capabilities), then the contact line from PROFILE.header. "headerLinks" may contain only entries in PROFILE.allowed_links, and only when PROFILE says the employer qualifies.
|
|
16
|
+
- CAREER HIGHLIGHTS. Exactly five, immediately after the header. Each is a short bold "leadIn" (two to six words) plus a "proof" sentence. Where the highlight carries a metric, the leadIn IS the figure ("29.3% off annual freight."), not a capability label. A highlight with no figure, or one whose subject is leadership scope, keeps a capability label or its verb. Select per posting, never a fixed set; at least ONE answers the posting's hardest stated must have.
|
|
17
|
+
- HIGHLIGHTS MUST BE SUBSTANTIATED. Every metric phrase in a highlight must also appear, in the same words, in a body bullet under the role that earned it. The engine checks this mechanically (highlight_body_match). If length forces a choice, cut the claim from the highlight, never the evidence from the body.
|
|
18
|
+
- HIGHLIGHT LENGTH is a hard cap: leadIn plus proof fit two rendered lines, about 200 characters.
|
|
19
|
+
- SCOPE LINES. Any entry whose title a screener could misread gets a "scope" line, one or two clauses, that says what the role actually was. Leave it empty where the title speaks for itself. PROFILE.scope_notes lists the entries that need one and why.
|
|
20
|
+
- SECTION NAMING. "experienceSectionTitle" is one of PROFILE.section_titles, chosen to prime the reader for the posting's function; plain "Experience" when none fits or when a non-operational entry leads.
|
|
21
|
+
- RELEVANCE ORDER. Reverse chronology is the default. When one tenure is overwhelmingly the best match you MAY lead with it and keep the rest chronological beneath. Dates stay accurate and visible: reordering, never concealment.
|
|
22
|
+
- LENGTH. Two pages, verified by the renderer. Page one must independently carry the case: header, highlights, and the most relevant full employer entry.
|
|
23
|
+
|
|
24
|
+
## Selection
|
|
25
|
+
|
|
26
|
+
Within each role, rank the records by fit to the posting's frozen scoring categories, breaking ties by strength score. Consider records from ALL roles; do not let a few strong records from one company crowd out relevant experience from another. Never repeat a metric or theme across body bullets. A highlight and the bullet that backs it are not a repeat.
|
|
27
|
+
|
|
28
|
+
## Wording
|
|
29
|
+
|
|
30
|
+
- Start from each record's first listed wording. You MAY lightly rewrite to mirror the posting's terminology and lead with a strong verb.
|
|
31
|
+
- FACTUAL SAFETY IS ABSOLUTE. Use only metrics marked confirmed on that record, paired with the outcome they are recorded against. Never invent, round, combine, extrapolate, or strengthen a number. Never turn a team result into sole ownership. Never add a claim the record does not state. If unsure, use the stored wording as is.
|
|
32
|
+
- BULLET REGISTER. HOUSE RULES.bullet_register decides the shape of every body bullet. "result_first" (the default, RESULT FIRST): metric bearing bullets lead with the result, so a screener scanning the left edge hits numbers ("29.3% off annual freight by rebidding 40 lanes"). "action_first": the ownership verb leads and the result closes the sentence ("Rebid 40 lanes, taking 29.3% off annual freight"). "posting": read the posting's own responsibility lines; verb-led lines mean action_first, outcome-led lines mean result_first; say which you chose and why in selectionNotes. The same facts and the same claims either way; only word order moves. Leadership scope is exempt in every register: where the achievement IS span of control, keep the verb in front of the headcount.
|
|
33
|
+
- QUALIFIER WORDS ARE LOAD BEARING. "Grew into", "de facto", "approximately", and "roughly" carry factual weight; never drop them. Never go passive in a way that drops the actor where the actor is the point.
|
|
34
|
+
- OWNERSHIP VERB LADDER. "Owned" or "accountable for" only where the record confirms direct responsibility. "Led" or "directed" where the record confirms leading the work. "Grew into de facto leadership of" for real scope without the title. "Partnered with" for joint work. "Supported" or "contributed to" where someone else owned it. "Exposure to" for hands on contact without ownership. NEVER promote a claim up this ladder to match a posting keyword; the engine checks each ownership claim against the record's confirmed verbs. Demoting a verb the record already earned is equally wrong.
|
|
35
|
+
- TENURE. Never compute "N+ years" from dates. State tenure only through a record's confirmed verbatim claim, or let the dates speak. A duration attached to a confirmed metric ("in 9 months") is fine; a career aggregate is not.
|
|
36
|
+
- VOCABULARY TRANSLATION. Same facts, the employer's nouns. Never change a number, only the noun. Apply PROFILE.vocabulary_map by industry; keep native terms for employers in the candidate's own industry.
|
|
37
|
+
- SKILLS come only from the records' tags and skills relevant to the posting. Mirror the posting's vocabulary where the capability is genuinely evidenced; never list a system or standard the records do not support. When the employer builds AI products, keep capability terms and drop vendor brand names.
|
|
38
|
+
|
|
39
|
+
## House style
|
|
40
|
+
|
|
41
|
+
Apply PROFILE.style and the candidate's House Rules. The engine defaults: no dashes of any kind in outbound prose, date ranges as "Oct 2023 to Nov 2025", compounds written open, "ecommerce", "30+" not "30 plus", "P&L", the "%" symbol. Phone numbers keep their hyphens; they are identifiers.
|
|
42
|
+
|
|
43
|
+
## Claims
|
|
44
|
+
|
|
45
|
+
Every highlight proof and every bullet is claim bearing. Its "claims" array names, for each factual atom, the record it draws on: {"kind":"record","source_id":...} for attribution; {"kind":"metric","source_id":...,"value":"18%","outcome_key":...} for every figure, using the record's own outcome_key; {"kind":"ownership","source_id":...,"verb":"led","subject":"..."} for the lead ownership verb; {"kind":"tenure","source_id":...,"value":"selling since 2008"} for any tenure or duration phrase. Copy ids exactly as shown in the slice. A field with a figure and no metric claim is rejected. There is no date field anywhere in the output; the engine stamps dates.
|
|
46
|
+
|
|
47
|
+
## Output
|
|
48
|
+
|
|
49
|
+
Only the JSON object matching OUTPUT SCHEMA. No markdown fences, no commentary. "specialties" has exactly three entries, "careerHighlights" exactly five. "section" is "experience" or "concurrent". "selectionNotes" briefly explains the choices, including which highlight answers the hardest must have and how any AI or portfolio entry was placed.
|