@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.
Files changed (121) hide show
  1. package/README.md +99 -0
  2. package/bin/cli.js +628 -0
  3. package/bridge/byo_key.js +50 -0
  4. package/bridge/credits.js +72 -0
  5. package/bridge/paste.js +44 -0
  6. package/bridge/providers/anthropic.js +16 -0
  7. package/bridge/providers/gemini.js +15 -0
  8. package/bridge/providers/index.js +39 -0
  9. package/bridge/providers/openai_compatible.js +17 -0
  10. package/core/extract.js +55 -0
  11. package/core/generators/excerpt.js +64 -0
  12. package/core/generators/packet.js +197 -0
  13. package/core/generators/serialize.js +56 -0
  14. package/core/generators/slice.js +61 -0
  15. package/core/house_rules.js +49 -0
  16. package/core/kb_import.js +257 -0
  17. package/core/migrations/002-profile.sql +3 -0
  18. package/core/migrations/003-discover.sql +49 -0
  19. package/core/parsers.js +79 -0
  20. package/core/practice.js +75 -0
  21. package/core/provenance.js +90 -0
  22. package/core/qr.js +185 -0
  23. package/core/rules/categories.md +5 -0
  24. package/core/rules/deep_answers.md +7 -0
  25. package/core/rules/discover.md +30 -0
  26. package/core/rules/extract.md +31 -0
  27. package/core/rules/followup.md +9 -0
  28. package/core/rules/index.js +32 -0
  29. package/core/rules/onepager.md +15 -0
  30. package/core/rules/practice.md +10 -0
  31. package/core/rules/prep.md +47 -0
  32. package/core/rules/resume.md +49 -0
  33. package/core/rules/rubric.md +120 -0
  34. package/core/schema.sql +342 -0
  35. package/core/schemas/categories.js +16 -0
  36. package/core/schemas/cover.js +10 -0
  37. package/core/schemas/deep_answers.js +16 -0
  38. package/core/schemas/discover.js +19 -0
  39. package/core/schemas/extract.js +32 -0
  40. package/core/schemas/followup.js +10 -0
  41. package/core/schemas/index.js +32 -0
  42. package/core/schemas/lang.js +111 -0
  43. package/core/schemas/onepager.js +26 -0
  44. package/core/schemas/practice.js +18 -0
  45. package/core/schemas/prep.js +15 -0
  46. package/core/schemas/resume.js +25 -0
  47. package/core/scoring.js +586 -0
  48. package/core/text.js +98 -0
  49. package/core/tracker_map.js +94 -0
  50. package/core/ulid.js +38 -0
  51. package/core/validators/highlight_body_match.js +49 -0
  52. package/core/validators/index.js +43 -0
  53. package/core/validators/metric_pairing.js +52 -0
  54. package/core/validators/no_confabulated_history.js +63 -0
  55. package/core/validators/no_dashes.js +31 -0
  56. package/core/validators/no_derived_tenure.js +60 -0
  57. package/core/validators/style_config.js +18 -0
  58. package/core/validators/verb_ladder.js +51 -0
  59. package/core/verbs.js +43 -0
  60. package/http/api.js +260 -0
  61. package/http/guard.js +106 -0
  62. package/http/headers.js +17 -0
  63. package/http/hosted.js +273 -0
  64. package/http/identity.js +70 -0
  65. package/http/server.js +136 -0
  66. package/http/sse.js +41 -0
  67. package/mcp/server.js +25 -0
  68. package/mcp/tools.js +237 -0
  69. package/package.json +59 -0
  70. package/store/backup.js +63 -0
  71. package/store/backup_dir.js +60 -0
  72. package/store/config.js +32 -0
  73. package/store/context.js +74 -0
  74. package/store/db.js +37 -0
  75. package/store/discover.js +120 -0
  76. package/store/events.js +28 -0
  77. package/store/export_xlsx.js +62 -0
  78. package/store/home.js +36 -0
  79. package/store/import_kb.js +88 -0
  80. package/store/import_tracker.js +195 -0
  81. package/store/ingest.js +140 -0
  82. package/store/kb.js +202 -0
  83. package/store/log.js +88 -0
  84. package/store/migrate.js +55 -0
  85. package/store/onboarding.js +267 -0
  86. package/store/packets.js +110 -0
  87. package/store/practice.js +52 -0
  88. package/store/proposals.js +119 -0
  89. package/store/reads.js +110 -0
  90. package/store/render.js +111 -0
  91. package/store/reply.js +19 -0
  92. package/store/review.js +64 -0
  93. package/store/scoring.js +134 -0
  94. package/store/settings.js +142 -0
  95. package/store/snapshot.js +88 -0
  96. package/store/transitions.js +391 -0
  97. package/store/tx.js +71 -0
  98. package/store/tz.js +24 -0
  99. package/ui/app.css +137 -0
  100. package/ui/app.js +75 -0
  101. package/ui/fonts/plexmono-400.woff2 +0 -0
  102. package/ui/fonts/plexmono-500.woff2 +0 -0
  103. package/ui/fonts/plexmono-600.woff2 +0 -0
  104. package/ui/fonts/spacegrotesk-var.woff2 +0 -0
  105. package/ui/hosted/signin.css +8 -0
  106. package/ui/hosted/signin.html +30 -0
  107. package/ui/hosted/signin.js +28 -0
  108. package/ui/icon-192.png +0 -0
  109. package/ui/icon-512.png +0 -0
  110. package/ui/index.html +20 -0
  111. package/ui/lib.js +92 -0
  112. package/ui/manifest.webmanifest +18 -0
  113. package/ui/screens/application.js +73 -0
  114. package/ui/screens/dashboard.js +37 -0
  115. package/ui/screens/document.js +64 -0
  116. package/ui/screens/interview.js +85 -0
  117. package/ui/screens/kb.js +142 -0
  118. package/ui/screens/packet.js +122 -0
  119. package/ui/screens/postings.js +92 -0
  120. package/ui/screens/settings.js +115 -0
  121. package/ui/vendor/preact.mjs +1 -0
package/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # gafj
2
+
3
+ GAF-J, the local campaign engine. One SQLite file on your PC, one ingest door for generated text, your own AI subscription. The architecture plan is at `../docs/career-engine-plan.md`; this folder is its implementation, built phase by phase.
4
+
5
+ Status: phases 1 through 6 built and tested (82 public tests on the synthetic candidates, 7 private calibration tests on the operator's set, 24 tests under `../functions/test` including the phase 7 relay against fakes); phase 7 is written but not deployed, per the plan's separate go decision. Nothing below has been run on the operator's Windows PC yet except the phase 1 import. What exists:
6
+
7
+ - `core/schema.sql` schema version 1 (plan section K); `core/migrations/002-profile.sql` adds `candidate.profile_json`
8
+ - `core/scoring.js` verbatim from the Firebase function, with three text helpers added to its exports
9
+ - `core/kb_import.js` legacy knowledge base to accomplishment rows: metric cleanup, outcome keys, verbs, duration and tenure claims, and the fixture rule for per-dimension review state (a dimension is confirmed only when a fixture line exercises it on that record)
10
+ - `core/rules/*.md` the engine rules per document kind, candidate-neutral; `core/rules/index.js` versions them
11
+ - `core/schemas/` the output schemas in a small schema language (string classes, claim-bearing fields, counts) with a JSON Schema export
12
+ - `core/provenance.js` the claim check: every claim resolves to a cited record's confirmed dimensions or to this application's interviews, documents, and people
13
+ - `core/validators/` the ledger as seven functions: `no_dashes`, `no_derived_tenure`, `highlight_body_match`, `metric_pairing`, `verb_ladder`, `style_config`, `no_confabulated_history`. Style rules warn or block per kind and house settings; integrity rules always block
14
+ - `core/house_rules.js` style settings with opt-in allowances; `core/text.js` figure extraction and identifier stripping
15
+ - `core/tracker_map.js` tracker rows to store shapes; `store/import_tracker.js` idempotent import with a rounds manifest for what the tracker holds as prose
16
+ - `store/` open with the documented PRAGMA order, migrations under a lock, `withTx` with BUSY retry, append-only events with actors, IANA zone check, home folder with a sync-folder warning, snapshot, xlsx export, backup and restore, `context.js` (the slice and application universe a check runs against), `ingest.js` (the only door onto the document table; three attempts per run then stop), `review.js` (confirm, edit, drop a dimension; bumps the kb revision)
17
+ - `core/generators/` the packet builders: `serialize.js` (one record, confirmed dimensions only, bounded), `slice.js` (precedence: pinned, then evidence-cited, then the top five by rankEvidence per category at weight 7 or above under a byte budget; a required set that overflows fails closed), `excerpt.js` (title, location, comp, evidence lines, a bounded body window; never the raw text), `packet.js` (one semantic packet per operation: rules, context, slice with ids, output JSON Schema; refuses without frozen scoring; prep-family packets carry this application's facts only)
18
+ - `bridge/paste.js` the paste route: the packet as one text file with the run handle at the top
19
+ - `store/packets.js` gathers the facts and opens the run (ai_run pending with packet hash and kb ids; ingest uses those ids as the slice); `store/scoring.js` the scoring contract (`proposeFromScan`, `freezeScoring`: verbatim evidence or rejected, evidence ranked with scoring.js unchanged, weighted mean, verdict, evidence hash, supersedes); `store/render.js` Markdown export of a passed document with the round and people from the store
20
+ - `mcp/` the stdio MCP server (phase 3) on `@modelcontextprotocol/server` 2.0.0: read tools (`campaign_snapshot`, `list_interviews`, `list_postings`, `get_posting`, `get_scoring`, `list_accomplishments`, `get_person`, `get_<op>_packet`), the one door (`save_document`, isError when blocked, stop after three attempts), and the proposal tools through `store/proposals.js` (`store_posting` proposed and idempotent, `propose_categories`, `kb_gap_request`, `propose_accomplishment`, `export_tracker_xlsx`). Nothing on the wire confirms, schedules, sends, or applies
21
+ - `http/` the local server (phase 4): 127.0.0.1 only, Host check, session cookie on every /api call (HttpOnly, SameSite=Strict, minted from a single-use launch nonce), Origin and JSON checks on mutations, 1 MB body cap, CSP with no remote scripts, SSE change feed on `/api/events` polled from the event log so MCP writes show up too. `store/transitions.js` holds the UI-only state machines (section M); `store/reads.js` the screen read models
22
+ - `ui/` Preact and htm vendored as one file (`ui/vendor/preact.mjs`), no build step: dashboard (screen 1), application (3), interview hub (4), document view with print CSS, and the packet modal with the form-based repair editor (7)
23
+ - Phase 5: screens 2 (postings: paste, store and scan locally, edit and freeze categories, log the application), 5 (knowledge base: gaps to answer, pending rows to confirm or dismiss, imported dimensions to review, search), 6 (settings: route, providers with write-only keys, house rules per kind, thresholds including the export folder, backup and restore test). `store/kb.js` holds the only confirm path after import; `store/settings.js` the config writes; `core/practice.js` and `store/practice.js` the drill workbook with planted anomalies, an AnswerKey tab, and a Solution tab, written with exceljs. The four paper moments run as scripts in `test/moments.test.js`
24
+ - Phase 6: `bridge/byo_key.js` and `bridge/providers/{anthropic,openai_compatible,gemini}.js`, fetch only, https unless loopback, no redirects, keys scrubbed from every error; `POST /api/run/:op` runs a packet through the active provider and straight into the door
25
+ - Phase 5b, onboarding (plan section U): `core/parsers.js` extracts text from docx (mammoth), pdf (unpdf text layer), md, and txt under caps (raw size, docx entries and inflated bytes, PDF pages, timeout); an empty result is stored and shown as a failure with the "OCR is not in v1" message. `core/schemas/extract.js` and `core/rules/extract.md` define the extraction packet; `store/onboarding.js` is propose_from_source: every factual atom must be a verbatim excerpt of its source at the span it cites or the draft is rejected whole, pending rows carry batch, source, attempt, and excerpts, dedup suggests groups (same employer, overlapping dates, a shared figure) and the candidate merges or keeps separate by a click, style is proposed from the sources and never imposed. MCP adds `list_sources`, `get_extract_packet`, `save_extraction`. The synthetic one-resume candidate goes from upload to a confirmed knowledge base and then through the resume door in `test/onboarding.test.js`
26
+ - Roadmap close-out (plan sections O, Q, U, and phase 7 as code): `test/hostile.test.js` is the hostile corpus against both doors (malformed and truncated JSON, fences, arrays and primitives at the top, nulls and objects for arrays, unknown and smuggled fields, 2 MB bodies, 100k-deep nesting, `__proto__` and `constructor` keys, terminal escapes, spans outside or shifted against the source, homoglyphs, a fourth attempt), plus an `npm pack --dry-run` test that every shipped file is on the whitelist and none is a fixture, a database, or a log; `core/extract.js` caps content at 1 MB and nesting at 64 levels and rejects `__proto__` at any depth; the schema validator refuses control characters and looks fields up as own properties (a `constructor` key used to pass as known). `store/log.js` is the operational log at `HOME/logs/gafj.jsonl`, keys fixed to `ts, level, event, entity, entity_id, actor, ms, status`, values cut at 200 characters, rotated past 10 MB with three kept; `serve` writes one line per request (method, path shape, status, ms; launch nonces collapsed) and never a body. `store/kb.js listHoles` computes the questions the records themselves raise (a record with no confirmed figure; an ownership verb at the top tier with no scope figure) with a prefilled draft; the answer lands pending with `replaces`, and confirming it extends that record's confirmed dimensions instead of adding a twin (screen 5, "Holes"). Phase 7 as code, not deployed: `../functions/relay/` (ledger, Stripe signature and event-id idempotency, operation endpoints with every cloud dependency injected, `functions/test/relay.test.js` runs the stop gate against fakes) and the local half `bridge/credits.js` with the relay url and sign-in token in config.json (masked on read), `POST /api/run/:op` on the credits route, `GET /api/credits/balance`, and the Credits card on screen 6
27
+ - Discovery, the shakedown after upload (plan U): `discover` is a candidate-level op (schema v3 rebuilds `ai_run` for it; `store/migrate.js` now runs the documented foreign-keys-off recipe with `foreign_key_check` before commit). `core/rules/discover.md` and `core/schemas/discover.js`: the AI reads the confirmed records, thinnest first with the engine's holes and every prior question in context, and returns questions only, each tied to a record id in the slice. `store/discover.js` is the door: a question about a record outside the packet is refused whole, duplicates drop, twelve open at most, three attempts then stop, and a new round is refused while questions are open. The candidate answers on screen 5 ("Discovery"); the answer is a pending row with `replaces`, so confirming it extends the record. `store/reply.js` picks the door by the run's operation for the UI, the CLI (`gafj packet discover`, `gafj ingest --run`), byo_key and credits runs; MCP adds `get_discover_packet`, `save_discovery`, `list_questions`.
28
+ - Bullet register (screen 6): `house_rules.bullet_register` is `result_first` (default, RAS), `action_first`, or `posting`; it travels in the resume packet's HOUSE RULES and `core/rules/resume.md` says what each means. Same facts, same claims, same gate; only word order.
29
+ - Backup folder (screen 6): `config.backup_dir`, meant for a synced folder (Drive, OneDrive, Dropbox); `store/backup_dir.js` writes dated consistent copies there (`gafj backup`, the Back up now button, and one a day at `serve` start), keeps the newest 14, never prunes a pre-restore copy, and refuses the data folder itself. The live store never goes to a synced folder.
30
+ - Hosted tier (plan W, gates 2 and 4 as code): `http/hosted.js` runs the same routes, screens, and doors behind a Google sign-in with one store per user under `<root>/tenants/<uid>/`; the session names the uid, the uid names the folder, and no query spans tenants (`test/hosted.test.js` drives every parameterized route as user B with user A's ids and expects 404 and no leaked text). Provider keys, MCP config, and the restore test are 404 for hosted users; the model is reached through the relay with a service key and the uid (`x-gafj-service`, `x-gafj-uid`; the relay's second auth path), never a stored user token. `/account/export` streams a restorable backup, `/account/delete` removes the folder and every session. `http/identity.js` verifies Firebase ID tokens with node:crypto against Google's JWK set, no SDK. `ui/hosted/signin.html` is the one page with a loosened CSP (Firebase Auth from gstatic). `gafj serve-hosted` is configured by environment only; `deploy/hosted/` holds the Dockerfile and fly.toml; `functions/relay/deploy.js` builds the relay with Firebase Admin, Stripe, and the Anthropic SDK and `functions/index.js` exports it as `relay`. Nothing is deployed; `docs/operator-setup.md` is the list of what only the operator can do.
31
+ - `bin/cli.js`: `migrate`, `import`, `snapshot`, `review`, `lint`, `list`, `score-freeze`, `packet`, `ingest`, `render`, `export`, `backup`, `restore`, `mcp`, `print-mcp-config`, `serve`, `url`, `rotate-token`, `install-startup`, `allow-host`, `init`, `onboard`; every other command names its phase and exits
32
+
33
+ Onboarding another candidate, phase 5b:
34
+
35
+ ```
36
+ gafj init --name "Priya Natarajan" a fresh candidate and an open batch (GAFJ_HOME picks the folder)
37
+ gafj onboard add resume.docx old-resume.pdf notes.md
38
+ gafj onboard packet --source <id> an extraction packet to paste into any chat
39
+ gafj onboard ingest --run <run> --file reply.json
40
+ gafj onboard dedup suggested duplicate groups; gafj onboard merge <group> [--keep <pending id>]
41
+ gafj review or the knowledge base screen: confirm, edit, drop, each beside its excerpt
42
+ ```
43
+
44
+ The paste loop, phase 2:
45
+
46
+ ```
47
+ gafj list postings ids and whether scoring is frozen
48
+ gafj score-freeze --posting-id <id> --from-scan --propose-only writes a categories file to edit
49
+ gafj score-freeze --posting-id <id> --categories <file> [--scores <file>]
50
+ gafj list interviews
51
+ gafj packet prep --interview-id <id> writes packets/packet-prep-<run>.md; paste it into any chat
52
+ gafj ingest --run <run> --file reply.json blocks name the field, the span, and the nearest records; three attempts then stop
53
+ gafj render --document <id> Markdown of a passed document under documents/
54
+ ```
55
+
56
+ The UI, phase 4:
57
+
58
+ ```
59
+ gafj serve starts the local server (serve.lock holds pid and port), prints a launch URL, opens the browser
60
+ gafj url a fresh launch URL for the running server (each one works once)
61
+ gafj install-startup registers serve at login (schtasks on Windows, launchd on macOS, a systemd user unit elsewhere); --print shows the commands only
62
+ ```
63
+
64
+ On your phone (same store, same screens, over your own private network):
65
+
66
+ ```
67
+ 1. Install Tailscale on the PC and the phone, same account (free personal plan).
68
+ 2. On the PC: tailscale serve --bg http://127.0.0.1:7311 (your gafj port; prints a name like gafj-pc.tail1234.ts.net)
69
+ 3. gafj allow-host gafj-pc.tail1234.ts.net then restart gafj serve
70
+ 4. gafj url --host gafj-pc.tail1234.ts.net --qr scan the code with the phone, or type the link once
71
+ 5. On the phone: Chrome or Safari, share or menu, "Add to Home Screen" / "Install app".
72
+ ```
73
+
74
+ Tailscale terminates HTTPS on the PC and forwards to the loopback server, so nothing is exposed to the internet and the phone is a real installed app with a Secure cookie. Only hosts on the allow list are answered; every other Host header is still refused. The PC must be on for the phone to work: the store never leaves it.
75
+
76
+ Connecting Claude Desktop or Claude Code, phase 3:
77
+
78
+ ```
79
+ gafj print-mcp-config prints the Desktop snippet and the Claude Code command with absolute paths (never npx)
80
+ ```
81
+
82
+ A tracker-imported scoring row is frozen with no categories; packets for it rank the slice against the posting text instead, and `score-freeze --from-scan` replaces it when you want categories.
83
+
84
+ One deviation from the plan text: the highlight check (ledger rule 2) uses the shipped scoring function's semantics, a figure key match in the body or a different figure from the same record, rather than a phrase match. The phrase rule blocked every live resume; the key rule catches the missing-evidence case the rule exists for.
85
+
86
+ Private calibration gate (needs the candidate's own files, never committed):
87
+
88
+ ```
89
+ GAFJ_FIXTURES_PRIVATE=<folder> GAFJ_KB_DIR=<folder with accomplishments.json> npm run test:private
90
+ ```
91
+
92
+ Run the tests:
93
+
94
+ ```
95
+ cd gafj
96
+ npm test
97
+ ```
98
+
99
+ Node 24 or newer. Dependencies: `exceljs` (tracker import and export), `@modelcontextprotocol/server` and `zod` (the MCP server), `preact` and `htm` (vendored into `ui/vendor`; the browser never loads from node_modules), `mammoth` and `unpdf` (text extraction for onboarding). `@modelcontextprotocol/client` is a dev dependency for the stdio test. Never put candidate data in this folder; real fixtures live outside the repository (see `GAFJ_FIXTURES_PRIVATE` in the plan).