career-compass-mcp 2.3.0 → 2.4.0

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 (59) hide show
  1. package/README.md +98 -12
  2. package/build/bin/cli.d.ts.map +1 -1
  3. package/build/bin/cli.js +70 -7
  4. package/build/bin/cli.js.map +1 -1
  5. package/build/src/dashboard-lite/render.d.ts +11 -11
  6. package/build/src/dashboard-lite/render.d.ts.map +1 -1
  7. package/build/src/dashboard-lite/render.js +40 -26
  8. package/build/src/dashboard-lite/render.js.map +1 -1
  9. package/build/src/dashboard-lite/server.d.ts +5 -6
  10. package/build/src/dashboard-lite/server.d.ts.map +1 -1
  11. package/build/src/dashboard-lite/server.js +75 -2
  12. package/build/src/dashboard-lite/server.js.map +1 -1
  13. package/build/src/pipeline-stats.d.ts +59 -0
  14. package/build/src/pipeline-stats.d.ts.map +1 -0
  15. package/build/src/pipeline-stats.js +66 -0
  16. package/build/src/pipeline-stats.js.map +1 -0
  17. package/build/src/sample-data.d.ts +49 -0
  18. package/build/src/sample-data.d.ts.map +1 -0
  19. package/build/src/sample-data.js +234 -0
  20. package/build/src/sample-data.js.map +1 -0
  21. package/build/src/schemas/career-schema.d.ts +15 -4
  22. package/build/src/schemas/career-schema.d.ts.map +1 -1
  23. package/build/src/schemas/career-schema.js +36 -2
  24. package/build/src/schemas/career-schema.js.map +1 -1
  25. package/build/src/server.d.ts +14 -1
  26. package/build/src/server.d.ts.map +1 -1
  27. package/build/src/server.js +4 -1
  28. package/build/src/server.js.map +1 -1
  29. package/build/src/storage/file-store.d.ts +10 -0
  30. package/build/src/storage/file-store.d.ts.map +1 -1
  31. package/build/src/storage/file-store.js +72 -5
  32. package/build/src/storage/file-store.js.map +1 -1
  33. package/build/src/tools/career-kb.d.ts +2 -0
  34. package/build/src/tools/career-kb.d.ts.map +1 -1
  35. package/build/src/tools/career-kb.js +71 -12
  36. package/build/src/tools/career-kb.js.map +1 -1
  37. package/build/src/tools/doctor.d.ts +115 -0
  38. package/build/src/tools/doctor.d.ts.map +1 -0
  39. package/build/src/tools/doctor.js +549 -0
  40. package/build/src/tools/doctor.js.map +1 -0
  41. package/build/src/tools/interview.d.ts.map +1 -1
  42. package/build/src/tools/interview.js +206 -0
  43. package/build/src/tools/interview.js.map +1 -1
  44. package/build/src/tools/opportunity.d.ts.map +1 -1
  45. package/build/src/tools/opportunity.js +101 -22
  46. package/build/src/tools/opportunity.js.map +1 -1
  47. package/build/src/tools/pipeline.d.ts +23 -1
  48. package/build/src/tools/pipeline.d.ts.map +1 -1
  49. package/build/src/tools/pipeline.js +110 -17
  50. package/build/src/tools/pipeline.js.map +1 -1
  51. package/build/src/types/tool-args.d.ts +4 -1
  52. package/build/src/types/tool-args.d.ts.map +1 -1
  53. package/build/src/version.d.ts +9 -0
  54. package/build/src/version.d.ts.map +1 -1
  55. package/build/src/version.js +10 -1
  56. package/build/src/version.js.map +1 -1
  57. package/data/example/career/journal.yaml +4 -4
  58. package/data/example/pipeline/applications.yaml +7 -7
  59. package/package.json +5 -2
@@ -1,5 +1,5 @@
1
1
  import { createServer } from "http";
2
- import { loadPipeline, isCorruptDataError } from "../storage/file-store.js";
2
+ import { getDataDir, loadPipeline, isCorruptDataError } from "../storage/file-store.js";
3
3
  import { renderLiteDashboard } from "./render.js";
4
4
  /**
5
5
  * Zero-build "lite" dashboard server.
@@ -10,8 +10,79 @@ import { renderLiteDashboard } from "./render.js";
10
10
  * of ~/.career-compass, mirroring the force-dynamic guarantee of the Next.js
11
11
  * dashboard. Ships in the npm package (pure JS, no framework, no build).
12
12
  */
13
+ /**
14
+ * Host names this server will answer to. Everything else is refused.
15
+ *
16
+ * Binding loopback stops the *network* from reaching the dashboard; it does not
17
+ * stop a web page. Any site the user visits can point a hostname it controls at
18
+ * 127.0.0.1 and have the browser issue same-origin requests to this server —
19
+ * DNS rebinding. The one thing that distinguishes those requests from a real
20
+ * one is the `Host` header, which carries the attacker's name rather than a
21
+ * loopback name, so checking it is the whole defense.
22
+ *
23
+ * The dashboard serves a user's entire job search — companies, salaries,
24
+ * contacts, interview notes — from a machine-local origin with no auth, which
25
+ * is exactly the shape of target rebinding exists to reach.
26
+ */
27
+ const ALLOWED_HOSTNAMES = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
28
+ /**
29
+ * Split the hostname out of a `Host` header value, or reject the value.
30
+ *
31
+ * `Host` is `name[":" port]`, but a bracketed IPv6 literal (`[::1]:3141`) and a
32
+ * bare one (`::1`, which some clients send) both contain colons, so a naive
33
+ * split on ":" turns `[::1]` into `[`.
34
+ *
35
+ * Everything after the name is checked rather than discarded. Taking the left
36
+ * half of the first colon and asking no questions about the right half let
37
+ * `localhost:evil.com` and `[::1]evil.com` read as loopback. Nothing can put
38
+ * those bytes on the wire from a browser — `Host` is built from the URL
39
+ * authority, so a rebound page still sends its own name — but a parser that
40
+ * answers a question it was not asked is one refactor away from mattering.
41
+ * Only `:<digits>` is a port; anything else makes the header malformed, and
42
+ * malformed is not loopback.
43
+ */
44
+ function hostnameOf(host) {
45
+ const h = host.trim().toLowerCase();
46
+ if (!h)
47
+ return null;
48
+ // Bracketed IPv6 literal: nothing may follow the closing bracket but a port.
49
+ if (h.startsWith("[")) {
50
+ const bracketed = /^(\[[0-9a-f:.]+\])(?::\d+)?$/.exec(h);
51
+ return bracketed ? bracketed[1] : null;
52
+ }
53
+ // More than one colon and no brackets: a bare IPv6 literal, which cannot
54
+ // carry a port — the whole value is the hostname, so every character of it
55
+ // has to be one an address can contain.
56
+ if (h.split(":").length - 1 > 1) {
57
+ return /^[0-9a-f:.]+$/.test(h) ? h : null;
58
+ }
59
+ const named = /^([^:]+)(?::\d+)?$/.exec(h);
60
+ return named ? named[1] : null;
61
+ }
62
+ /**
63
+ * Is this `Host` header one of ours?
64
+ *
65
+ * A missing header is refused too. HTTP/1.1 requires it and every browser sends
66
+ * it, so its absence is either a hand-written request or an attempt to skip the
67
+ * check — neither is a case worth serving a career history to.
68
+ */
69
+ export function isAllowedHost(host) {
70
+ if (!host)
71
+ return false;
72
+ const hostname = hostnameOf(host);
73
+ return hostname !== null && ALLOWED_HOSTNAMES.has(hostname);
74
+ }
13
75
  export function createLiteDashboardServer() {
14
76
  return createServer(async (req, res) => {
77
+ // Checked before anything else, including the path: a rejected origin must
78
+ // not learn which paths exist or how the server responds to them.
79
+ if (!isAllowedHost(req.headers.host)) {
80
+ res.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
81
+ res.end(`Refused: this dashboard only answers to ${[...ALLOWED_HOSTNAMES].join(", ")}.\n` +
82
+ `The request arrived with Host: ${req.headers.host ?? "(none)"}.\n\n` +
83
+ `It binds loopback and is meant to be opened at http://localhost:<port> on this machine.`);
84
+ return;
85
+ }
15
86
  // Only serve the dashboard at "/"; everything else 404s (favicon, etc.).
16
87
  const path = (req.url ?? "/").split("?")[0];
17
88
  if (path !== "/" && path !== "/index.html") {
@@ -21,7 +92,9 @@ export function createLiteDashboardServer() {
21
92
  }
22
93
  try {
23
94
  const pipeline = await loadPipeline();
24
- const html = renderLiteDashboard(pipeline);
95
+ // Read per request, like the pipeline itself: the footer names the folder
96
+ // this page is actually serving, not the default one.
97
+ const html = renderLiteDashboard(pipeline, getDataDir());
25
98
  res.writeHead(200, {
26
99
  "content-type": "text/html; charset=utf-8",
27
100
  // Never cache: the whole point is a live read of local data.
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../../../src/dashboard-lite/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAe,MAAM,MAAM,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAElD;;;;;;;;GAQG;AACH,MAAM,UAAU,yBAAyB;IACvC,OAAO,YAAY,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QACrC,yEAAyE;QACzE,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5C,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,aAAa,EAAE,CAAC;YAC3C,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,CAAC;YACrD,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;QACD,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,YAAY,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,mBAAmB,CAAC,QAAQ,CAAC,CAAC;YAC3C,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE;gBACjB,cAAc,EAAE,0BAA0B;gBAC1C,6DAA6D;gBAC7D,eAAe,EAAE,2BAA2B;aAC7C,CAAC,CAAC;YACH,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAChB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,OAAO,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;YACxC,MAAM,GAAG,GAAG,OAAO;gBACjB,CAAC,CAAC,uGAAwG,GAAa,CAAC,OAAO,EAAE;gBACjI,CAAC,CAAC,+BAAgC,GAAa,EAAE,OAAO,IAAI,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;YAC5E,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,2BAA2B,EAAE,CAAC,CAAC;YACpF,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACf,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,QAAQ,GAAG,WAAW;IACrE,MAAM,MAAM,GAAG,yBAAyB,EAAE,CAAC;IAC3C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC7B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE;YACjC,IAAI,QAAQ,KAAK,WAAW;gBAAE,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC;YACrD,wEAAwE;YACxE,oDAAoD;YACpD,MAAM,EAAE,GAAG,yBAAyB,EAAE,CAAC;YACvC,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,mCAAmC;YAC5E,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE;gBAC1B,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC;gBACvC,OAAO,CAAC,MAAM,CAAC,CAAC;YAClB,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../../src/dashboard-lite/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ import type { Application, ApplicationStatus } from "./schemas/career-schema.js";
2
+ /**
3
+ * The one place a pipeline turns into numbers.
4
+ *
5
+ * `pipeline_view action=stats` and the dashboard both answer "what is my
6
+ * response rate?", and they answered it differently: the tool computed
7
+ * (total − applied) / total, which counts a role you have only *discovered* —
8
+ * and never sent anything to — as an employer response, and divides by a
9
+ * denominator that includes it. On the bundled sample that reported 75% while
10
+ * the dashboard, doing responded / sent, reported 71%. Two surfaces of one
11
+ * product disagreeing about the user's own number is worse than either being
12
+ * wrong: there is no way for them to tell which to believe.
13
+ *
14
+ * So the arithmetic lives here and both call it. Adding a stage to the funnel
15
+ * is now one edit rather than a hunt for every place a status list was inlined.
16
+ */
17
+ /**
18
+ * Stages a search is still live in — nothing has closed the door yet.
19
+ *
20
+ * `discovered` belongs here: a role you have found and not yet applied to is
21
+ * live work, and the board shows it as a column.
22
+ */
23
+ export declare const ACTIVE_STATUSES: readonly ApplicationStatus[];
24
+ /**
25
+ * Did this application actually get sent?
26
+ *
27
+ * Everything past `discovered` did. This is the denominator for both rates
28
+ * below, because a role you never applied to cannot answer you and cannot
29
+ * ghost you — including it only dilutes the number you were asking about.
30
+ */
31
+ export declare function wasSent(app: Application): boolean;
32
+ /**
33
+ * Did the employer come back?
34
+ *
35
+ * Anything past `applied` means someone on the other side acted, except
36
+ * `ghosted`, which is the recorded absence of exactly that.
37
+ *
38
+ * `withdrawn` counts as a response, which is right in the ordinary case — you
39
+ * withdraw from a process you are in — and generous in the rarer one where
40
+ * someone withdraws an application nobody ever answered. The stored data does
41
+ * not distinguish those, and the alternative (silently dropping withdrawals
42
+ * from the numerator) understates real conversations.
43
+ */
44
+ export declare function gotResponse(app: Application): boolean;
45
+ export interface PipelineStats {
46
+ total: number;
47
+ /** Applications actually submitted — the denominator for both rates. */
48
+ sent: number;
49
+ active: number;
50
+ inConversation: number;
51
+ offers: number;
52
+ ghosted: number;
53
+ /** Percent of sent applications the employer answered, 0 when none sent. */
54
+ responseRate: number;
55
+ /** Percent of sent applications that went silent, 0 when none sent. */
56
+ ghostRate: number;
57
+ }
58
+ export declare function computeStats(apps: Application[]): PipelineStats;
59
+ //# sourceMappingURL=pipeline-stats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline-stats.d.ts","sourceRoot":"","sources":["../../src/pipeline-stats.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAEjF;;;;;;;;;;;;;;GAcG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,iBAAiB,EAEvD,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAEjD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAErD;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,YAAY,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,aAAa,CAgB/D"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The one place a pipeline turns into numbers.
3
+ *
4
+ * `pipeline_view action=stats` and the dashboard both answer "what is my
5
+ * response rate?", and they answered it differently: the tool computed
6
+ * (total − applied) / total, which counts a role you have only *discovered* —
7
+ * and never sent anything to — as an employer response, and divides by a
8
+ * denominator that includes it. On the bundled sample that reported 75% while
9
+ * the dashboard, doing responded / sent, reported 71%. Two surfaces of one
10
+ * product disagreeing about the user's own number is worse than either being
11
+ * wrong: there is no way for them to tell which to believe.
12
+ *
13
+ * So the arithmetic lives here and both call it. Adding a stage to the funnel
14
+ * is now one edit rather than a hunt for every place a status list was inlined.
15
+ */
16
+ /**
17
+ * Stages a search is still live in — nothing has closed the door yet.
18
+ *
19
+ * `discovered` belongs here: a role you have found and not yet applied to is
20
+ * live work, and the board shows it as a column.
21
+ */
22
+ export const ACTIVE_STATUSES = [
23
+ "discovered", "applied", "screening", "interviewing", "offer", "negotiating",
24
+ ];
25
+ /**
26
+ * Did this application actually get sent?
27
+ *
28
+ * Everything past `discovered` did. This is the denominator for both rates
29
+ * below, because a role you never applied to cannot answer you and cannot
30
+ * ghost you — including it only dilutes the number you were asking about.
31
+ */
32
+ export function wasSent(app) {
33
+ return app.status !== "discovered";
34
+ }
35
+ /**
36
+ * Did the employer come back?
37
+ *
38
+ * Anything past `applied` means someone on the other side acted, except
39
+ * `ghosted`, which is the recorded absence of exactly that.
40
+ *
41
+ * `withdrawn` counts as a response, which is right in the ordinary case — you
42
+ * withdraw from a process you are in — and generous in the rarer one where
43
+ * someone withdraws an application nobody ever answered. The stored data does
44
+ * not distinguish those, and the alternative (silently dropping withdrawals
45
+ * from the numerator) understates real conversations.
46
+ */
47
+ export function gotResponse(app) {
48
+ return !["discovered", "applied", "ghosted"].includes(app.status);
49
+ }
50
+ export function computeStats(apps) {
51
+ const sent = apps.filter(wasSent).length;
52
+ const responded = apps.filter(gotResponse).length;
53
+ const ghosted = apps.filter((a) => a.status === "ghosted").length;
54
+ const pct = (n) => (sent ? Math.round((n / sent) * 100) : 0);
55
+ return {
56
+ total: apps.length,
57
+ sent,
58
+ active: apps.filter((a) => ACTIVE_STATUSES.includes(a.status)).length,
59
+ inConversation: apps.filter((a) => ["screening", "interviewing"].includes(a.status)).length,
60
+ offers: apps.filter((a) => ["offer", "negotiating", "accepted"].includes(a.status)).length,
61
+ ghosted,
62
+ responseRate: pct(responded),
63
+ ghostRate: pct(ghosted),
64
+ };
65
+ }
66
+ //# sourceMappingURL=pipeline-stats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline-stats.js","sourceRoot":"","sources":["../../src/pipeline-stats.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAiC;IAC3D,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,OAAO,EAAE,aAAa;CAC7E,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,OAAO,CAAC,GAAgB;IACtC,OAAO,GAAG,CAAC,MAAM,KAAK,YAAY,CAAC;AACrC,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,WAAW,CAAC,GAAgB;IAC1C,OAAO,CAAC,CAAC,YAAY,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACpE,CAAC;AAgBD,MAAM,UAAU,YAAY,CAAC,IAAmB;IAC9C,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;IACzC,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC;IAClD,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,MAAM,CAAC;IAClE,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAErE,OAAO;QACL,KAAK,EAAE,IAAI,CAAC,MAAM;QAClB,IAAI;QACJ,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;QACrE,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;QAC3F,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,aAAa,EAAE,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;QAC1F,OAAO;QACP,YAAY,EAAE,GAAG,CAAC,SAAS,CAAC;QAC5B,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC;KACxB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The bundled Alex Rivera sample, kept current.
3
+ *
4
+ * `data/example/` is the one populated Career KB anybody can look at without
5
+ * typing their own history in: the README points at it, the screenshots are
6
+ * taken from it, and `CAREER_DATA_PATH=data/example` is the documented way to
7
+ * see the dashboard with something in it. Every date in it was written by hand
8
+ * to represent mid-June 2026 — a panel "coming up" on the 17th, a follow-up
9
+ * four days past due. Nothing ages those, so the demo curdles: within weeks the
10
+ * interviews have already happened, the follow-ups are months overdue, and the
11
+ * first impression of a job-search tool is a search that was abandoned.
12
+ *
13
+ * Rather than re-date the YAML on a schedule (which only moves the expiry), the
14
+ * dates are shifted at read time by the distance between {@link sampleAnchor}
15
+ * — the sample's own notion of "today" — and the real today. The relative shape
16
+ * the sample was authored with is exactly preserved: the panel is still three
17
+ * days out, the rejection still landed a week ago. The files on disk are never
18
+ * touched, and only the copy inside this package is affected: a user's own data
19
+ * goes through untouched, whatever dates it has.
20
+ */
21
+ /**
22
+ * The anchor for a fixture this code cannot read.
23
+ *
24
+ * Only reachable when the sample directory is missing or unparseable, in which
25
+ * case there is nothing to shift and the value never gets used. It exists so
26
+ * the shift is a number rather than a special case.
27
+ */
28
+ export declare const SAMPLE_ANCHOR_FALLBACK = "2026-06-16";
29
+ /**
30
+ * Return a copy of `value` with every date shifted forward to sit around
31
+ * `today`. Pure: the input is never mutated, so a caller can hand it a parsed
32
+ * document and keep the original.
33
+ *
34
+ * `today` and `anchor` are injectable so tests are not a function of the day
35
+ * they run on, nor of the fixture's current contents.
36
+ */
37
+ export declare function freshenSampleDates<T>(value: T, today?: Date, anchor?: string): T;
38
+ export declare function bundledSampleDir(): string | null;
39
+ /** Is this data directory the demo that ships with the package? */
40
+ export declare function isBundledSampleDir(dir: string): boolean;
41
+ /**
42
+ * The sample's own "today": the newest moment anything in it was written.
43
+ *
44
+ * Read from the fixture rather than declared, so re-dating the YAML moves the
45
+ * anchor with it and the two cannot drift apart again. Cached — the fixture is
46
+ * immutable for the life of the process, and this runs on every sample read.
47
+ */
48
+ export declare function sampleAnchor(): string;
49
+ //# sourceMappingURL=sample-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sample-data.d.ts","sourceRoot":"","sources":["../../src/sample-data.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,eAAe,CAAC;AAwDnD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,KAAK,EAAE,CAAC,EACR,KAAK,GAAE,IAAiB,EACxB,MAAM,GAAE,MAAuB,GAC9B,CAAC,CAcH;AAmDD,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAGhD;AAED,mEAAmE;AACnE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGvD;AA6DD;;;;;;GAMG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAGrC"}
@@ -0,0 +1,234 @@
1
+ import { existsSync, readdirSync, readFileSync } from "fs";
2
+ import { dirname, join, resolve } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { parse as parseYaml } from "yaml";
5
+ /**
6
+ * The bundled Alex Rivera sample, kept current.
7
+ *
8
+ * `data/example/` is the one populated Career KB anybody can look at without
9
+ * typing their own history in: the README points at it, the screenshots are
10
+ * taken from it, and `CAREER_DATA_PATH=data/example` is the documented way to
11
+ * see the dashboard with something in it. Every date in it was written by hand
12
+ * to represent mid-June 2026 — a panel "coming up" on the 17th, a follow-up
13
+ * four days past due. Nothing ages those, so the demo curdles: within weeks the
14
+ * interviews have already happened, the follow-ups are months overdue, and the
15
+ * first impression of a job-search tool is a search that was abandoned.
16
+ *
17
+ * Rather than re-date the YAML on a schedule (which only moves the expiry), the
18
+ * dates are shifted at read time by the distance between {@link sampleAnchor}
19
+ * — the sample's own notion of "today" — and the real today. The relative shape
20
+ * the sample was authored with is exactly preserved: the panel is still three
21
+ * days out, the rejection still landed a week ago. The files on disk are never
22
+ * touched, and only the copy inside this package is affected: a user's own data
23
+ * goes through untouched, whatever dates it has.
24
+ */
25
+ /**
26
+ * The anchor for a fixture this code cannot read.
27
+ *
28
+ * Only reachable when the sample directory is missing or unparseable, in which
29
+ * case there is nothing to shift and the value never gets used. It exists so
30
+ * the shift is a number rather than a special case.
31
+ */
32
+ export const SAMPLE_ANCHOR_FALLBACK = "2026-06-16";
33
+ /**
34
+ * Fields that record *when a record was last written*, and nothing else.
35
+ *
36
+ * The anchor has to come from the fixture, because a hand-written constant is
37
+ * exactly what broke: `data/example/` was re-dated from March to June with a
38
+ * +81-day shift on its activity dates, and the anchor constant was set from a
39
+ * different reading of the same file. Every date then landed (today − 55) days
40
+ * on, which pushed the newest ones past today — a pipeline whose header claimed
41
+ * a "last write" seven weeks in the future and applications "updated tomorrow".
42
+ *
43
+ * These two keys are the only unambiguous ones. `followUpDue`, an interview
44
+ * round's `date`, and an offer's `expiresDate`/`startDate` are all *supposed* to
45
+ * sit in the future — anchoring on the newest of those would drag the whole
46
+ * demo into the past and produce the abandoned-search look this mechanism
47
+ * exists to prevent.
48
+ */
49
+ const ANCHOR_KEYS = new Set(["dateUpdated", "lastUpdated"]);
50
+ /** A whole-string date: `YYYY-MM-DD`, optionally followed by a time. */
51
+ const DATE_STRING = /^(\d{4})-(\d{2})-(\d{2})(.*)$/;
52
+ /** A date used as a note prefix: `[2026-06-01] Tailored resume…`. */
53
+ const NOTE_DATE = /\[(\d{4})-(\d{2})-(\d{2})\]/g;
54
+ function toUtcDay(y, m, d) {
55
+ return Date.UTC(y, m - 1, d);
56
+ }
57
+ function daysBetween(fromIso, to) {
58
+ const [y, m, d] = fromIso.split("-").map(Number);
59
+ // The target is read as a calendar day in the user's own timezone: "today"
60
+ // means the date on their wall, not a UTC instant.
61
+ const toDay = toUtcDay(to.getFullYear(), to.getMonth() + 1, to.getDate());
62
+ return Math.round((toDay - toUtcDay(y, m, d)) / 86400000);
63
+ }
64
+ function shiftDay(y, m, d, days) {
65
+ const shifted = new Date(toUtcDay(y, m, d) + days * 86400000);
66
+ return shifted.toISOString().slice(0, 10);
67
+ }
68
+ function shiftString(value, days) {
69
+ const whole = DATE_STRING.exec(value);
70
+ if (whole) {
71
+ const [, y, m, d, rest] = whole;
72
+ return shiftDay(Number(y), Number(m), Number(d), days) + rest;
73
+ }
74
+ // `YYYY-MM` (employment history) has no day and is not a moving target — a
75
+ // job that started in 2021-03 started in 2021-03 — so it is left alone by
76
+ // both patterns on purpose.
77
+ return value.replace(NOTE_DATE, (_all, y, m, d) => `[${shiftDay(Number(y), Number(m), Number(d), days)}]`);
78
+ }
79
+ /**
80
+ * Return a copy of `value` with every date shifted forward to sit around
81
+ * `today`. Pure: the input is never mutated, so a caller can hand it a parsed
82
+ * document and keep the original.
83
+ *
84
+ * `today` and `anchor` are injectable so tests are not a function of the day
85
+ * they run on, nor of the fixture's current contents.
86
+ */
87
+ export function freshenSampleDates(value, today = new Date(), anchor = sampleAnchor()) {
88
+ const days = daysBetween(anchor, today);
89
+ if (days === 0)
90
+ return value;
91
+ const walk = (node) => {
92
+ if (typeof node === "string")
93
+ return shiftString(node, days);
94
+ if (Array.isArray(node))
95
+ return node.map(walk);
96
+ if (node && typeof node === "object") {
97
+ return Object.fromEntries(Object.entries(node).map(([k, v]) => [k, walk(v)]));
98
+ }
99
+ return node;
100
+ };
101
+ return walk(value);
102
+ }
103
+ /**
104
+ * This module's own directory, or null where that question has no answer.
105
+ *
106
+ * `import.meta.url` is not always a `file:` URL — under the dashboard's browser
107
+ * -like test environment it is an `http:` one, and fileURLToPath throws on it.
108
+ * That threw out of a *load*, so an unrelated Career-KB read failed with
109
+ * "The URL must be of scheme file". Locating the demo is best-effort by nature;
110
+ * failing to locate it must never fail the caller.
111
+ */
112
+ function moduleDir() {
113
+ try {
114
+ return resolve(fileURLToPath(new URL(".", import.meta.url)));
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ }
120
+ /**
121
+ * Absolute path of the sample that ships inside this package.
122
+ *
123
+ * Resolved by walking up to our own package.json rather than a fixed number of
124
+ * `..` hops, because this module runs from `src/` under vitest and from
125
+ * `build/src/` when installed — the same reason version.ts walks. Falls back to
126
+ * the working directory when the module's own path is unavailable, which walks
127
+ * up to the same package.json from anywhere inside the project.
128
+ */
129
+ function findBundledSampleDir() {
130
+ let dir = moduleDir() ?? process.cwd();
131
+ for (;;) {
132
+ const pkg = join(dir, "package.json");
133
+ if (existsSync(pkg)) {
134
+ try {
135
+ const parsed = JSON.parse(readFileSync(pkg, "utf-8"));
136
+ if (parsed.name === "career-compass-mcp") {
137
+ const sample = join(dir, "data", "example");
138
+ return existsSync(sample) ? sample : null;
139
+ }
140
+ }
141
+ catch {
142
+ // Unreadable package.json — keep walking rather than give up here.
143
+ }
144
+ }
145
+ const parent = dirname(dir);
146
+ if (parent === dir)
147
+ return null;
148
+ dir = parent;
149
+ }
150
+ }
151
+ let cached;
152
+ export function bundledSampleDir() {
153
+ if (cached === undefined)
154
+ cached = findBundledSampleDir();
155
+ return cached;
156
+ }
157
+ /** Is this data directory the demo that ships with the package? */
158
+ export function isBundledSampleDir(dir) {
159
+ const sample = bundledSampleDir();
160
+ return sample !== null && resolve(dir) === resolve(sample);
161
+ }
162
+ // ─── Anchor derivation ────────────────────────────────────────────────────────
163
+ /**
164
+ * Every `dateUpdated` / `lastUpdated` value under a parsed document.
165
+ *
166
+ * `key` is the field the current node was reached through, which is what makes
167
+ * this selective: the same date string means "written on" under `dateUpdated`
168
+ * and "due on" under `followUpDue`, and only the first can bound the anchor.
169
+ * Array elements inherit their array's key, which is correct — the entries of
170
+ * a `notes:` list are still notes — and objects rebind it per field.
171
+ */
172
+ function collectAnchorDates(node, key, out) {
173
+ if (typeof node === "string") {
174
+ if (key !== null && ANCHOR_KEYS.has(key) && DATE_STRING.test(node))
175
+ out.push(node.slice(0, 10));
176
+ return;
177
+ }
178
+ if (Array.isArray(node)) {
179
+ for (const item of node)
180
+ collectAnchorDates(item, key, out);
181
+ return;
182
+ }
183
+ if (node && typeof node === "object") {
184
+ for (const [k, v] of Object.entries(node)) {
185
+ collectAnchorDates(v, k, out);
186
+ }
187
+ }
188
+ }
189
+ function findSampleAnchor() {
190
+ const root = bundledSampleDir();
191
+ if (!root)
192
+ return SAMPLE_ANCHOR_FALLBACK;
193
+ const found = [];
194
+ const walk = (dir) => {
195
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
196
+ const full = join(dir, entry.name);
197
+ if (entry.isDirectory()) {
198
+ walk(full);
199
+ }
200
+ else if (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml")) {
201
+ try {
202
+ collectAnchorDates(parseYaml(readFileSync(full, "utf-8")), null, found);
203
+ }
204
+ catch {
205
+ // One unparseable file must not cost the anchor. The loader reports
206
+ // that failure properly; this is a best-effort read for a demo.
207
+ }
208
+ }
209
+ }
210
+ };
211
+ try {
212
+ walk(root);
213
+ }
214
+ catch {
215
+ return SAMPLE_ANCHOR_FALLBACK;
216
+ }
217
+ if (found.length === 0)
218
+ return SAMPLE_ANCHOR_FALLBACK;
219
+ return found.reduce((a, b) => (a > b ? a : b));
220
+ }
221
+ let cachedAnchor;
222
+ /**
223
+ * The sample's own "today": the newest moment anything in it was written.
224
+ *
225
+ * Read from the fixture rather than declared, so re-dating the YAML moves the
226
+ * anchor with it and the two cannot drift apart again. Cached — the fixture is
227
+ * immutable for the life of the process, and this runs on every sample read.
228
+ */
229
+ export function sampleAnchor() {
230
+ if (cachedAnchor === undefined)
231
+ cachedAnchor = findSampleAnchor();
232
+ return cachedAnchor;
233
+ }
234
+ //# sourceMappingURL=sample-data.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sample-data.js","sourceRoot":"","sources":["../../src/sample-data.ts"],"names":[],"mappings":"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"}
@@ -12,6 +12,17 @@ export declare const ApplicationStatus: z.ZodEnum<{
12
12
  ghosted: "ghosted";
13
13
  }>;
14
14
  export type ApplicationStatus = z.infer<typeof ApplicationStatus>;
15
+ /**
16
+ * Every status, in funnel order — the order the stages are declared above.
17
+ *
18
+ * This is the canonical ordering for anything that needs to rank stages: the
19
+ * dashboard board, `pipeline_view sortBy=status`, and the transition check in
20
+ * pipeline.ts. Derived from the enum rather than retyped so a new stage cannot
21
+ * be added in one place and forgotten in the other.
22
+ */
23
+ export declare const STATUS_ORDER: readonly ApplicationStatus[];
24
+ /** Position of a status in the funnel. Lower is earlier. */
25
+ export declare function statusRank(status: ApplicationStatus): number;
15
26
  export declare const Achievement: z.ZodObject<{
16
27
  metric: z.ZodString;
17
28
  context: z.ZodString;
@@ -79,8 +90,8 @@ export declare const Profile: z.ZodObject<{
79
90
  salaryMin: z.ZodOptional<z.ZodNumber>;
80
91
  salaryMax: z.ZodOptional<z.ZodNumber>;
81
92
  salaryCurrency: z.ZodDefault<z.ZodString>;
82
- openToRemote: z.ZodDefault<z.ZodBoolean>;
83
- openToRelocation: z.ZodDefault<z.ZodBoolean>;
93
+ openToRemote: z.ZodOptional<z.ZodBoolean>;
94
+ openToRelocation: z.ZodOptional<z.ZodBoolean>;
84
95
  noticePeriod: z.ZodOptional<z.ZodString>;
85
96
  }, z.core.$strip>;
86
97
  /**
@@ -173,8 +184,8 @@ export declare const CareerData: z.ZodObject<{
173
184
  salaryMin: z.ZodOptional<z.ZodNumber>;
174
185
  salaryMax: z.ZodOptional<z.ZodNumber>;
175
186
  salaryCurrency: z.ZodDefault<z.ZodString>;
176
- openToRemote: z.ZodDefault<z.ZodBoolean>;
177
- openToRelocation: z.ZodDefault<z.ZodBoolean>;
187
+ openToRemote: z.ZodOptional<z.ZodBoolean>;
188
+ openToRelocation: z.ZodOptional<z.ZodBoolean>;
178
189
  noticePeriod: z.ZodOptional<z.ZodString>;
179
190
  }, z.core.$strip>;
180
191
  experience: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -1 +1 @@
1
- {"version":3,"file":"career-schema.d.ts","sourceRoot":"","sources":["../../../src/schemas/career-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,iBAAiB;;;;;;;;;;;EAW5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAIlE,eAAO,MAAM,WAAW;;;;;iBAKtB,CAAC;AAEH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;iBAUrB,CAAC;AAEH,eAAO,MAAM,KAAK;;;;;;iBAMhB,CAAC;AAEH,eAAO,MAAM,SAAS;;;;;;;iBAOpB,CAAC;AAEH,eAAO,MAAM,OAAO;;;;;;;;iBAQlB,CAAC;AAEH,eAAO,MAAM,WAAW;;;;;;iBAMtB,CAAC;AAEH,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;iBAiBlB,CAAC;AAIH;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2BvB,CAAC;AAEH,oFAAoF;AACpF,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAAwB,CAAC;AAIpD,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAQrB,CAAC;AAIH,eAAO,MAAM,OAAO;;;;;;iBAMlB,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;iBAMzB,CAAC;AAEH,eAAO,MAAM,KAAK;;;;;;;;;iBAShB,CAAC;AAEH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0BtB,CAAC;AAEH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAGnB,CAAC;AAIH,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,OAAO,CAAC,CAAC;AAC9C,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AACpD,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,KAAK,CAAC,CAAC;AAC1C,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAClD,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,OAAO,CAAC,CAAC;AAC9C,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AACtD,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AACtD,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AACpD,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AACxD,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AACtD,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC;AAChD,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,OAAO,CAAC,CAAC;AAC9C,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAC5D,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,KAAK,CAAC,CAAC"}
1
+ {"version":3,"file":"career-schema.d.ts","sourceRoot":"","sources":["../../../src/schemas/career-schema.ts"],"names":[],"mappings":"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"}
@@ -12,6 +12,19 @@ export const ApplicationStatus = z.enum([
12
12
  "withdrawn",
13
13
  "ghosted",
14
14
  ]);
15
+ /**
16
+ * Every status, in funnel order — the order the stages are declared above.
17
+ *
18
+ * This is the canonical ordering for anything that needs to rank stages: the
19
+ * dashboard board, `pipeline_view sortBy=status`, and the transition check in
20
+ * pipeline.ts. Derived from the enum rather than retyped so a new stage cannot
21
+ * be added in one place and forgotten in the other.
22
+ */
23
+ export const STATUS_ORDER = ApplicationStatus.options;
24
+ /** Position of a status in the funnel. Lower is earlier. */
25
+ export function statusRank(status) {
26
+ return STATUS_ORDER.indexOf(status);
27
+ }
15
28
  // ─── Career Data ───────────────────────────────────────────────────────────────
16
29
  export const Achievement = z.object({
17
30
  metric: z.string().describe("Quantifiable outcome, e.g. 'reduced latency 40%'"),
@@ -75,8 +88,29 @@ export const Profile = z.object({
75
88
  salaryMin: z.number().optional(),
76
89
  salaryMax: z.number().optional(),
77
90
  salaryCurrency: z.string().default("USD"),
78
- openToRemote: z.boolean().default(true),
79
- openToRelocation: z.boolean().default(false),
91
+ // `.optional()` rather than `.default(true)`/`.default(false)`.
92
+ //
93
+ // A zod default is applied at parse time, so once the profile is loaded a
94
+ // stated answer and an unanswered field are indistinguishable. That is
95
+ // harmless for a field nobody asserts anything about, and wrong for these two:
96
+ // the fit analysis prints them under "the hard constraints this fit must be
97
+ // checked against" and then treats a location mismatch as a blocker. A
98
+ // first-run profile is name + summary only — exactly what the documented
99
+ // resume-paste onboarding produces — so the defaults had the tool assert
100
+ // "Open to relocation: no" on the user's behalf and rule roles out on a
101
+ // preference they never expressed. Inventing a preference is the specific
102
+ // dishonesty this product exists to prevent.
103
+ //
104
+ // It also stopped absence being recoverable: `save_career_section` validates
105
+ // with this schema and writes `parsed.data`, so the baked default was
106
+ // persisted into profile.yaml on the first save and became indistinguishable
107
+ // from a real answer from then on.
108
+ //
109
+ // Consumers that need a plain boolean apply the old default at their own use
110
+ // site (`?? true` / `?? false`), so behavior for the absent case is unchanged
111
+ // everywhere except where the stated/unstated distinction is the point.
112
+ openToRemote: z.boolean().optional(),
113
+ openToRelocation: z.boolean().optional(),
80
114
  noticePeriod: z.string().optional().describe("e.g. '2 weeks', 'immediately'"),
81
115
  });
82
116
  // ─── Career Journal ─────────────────────────────────────────────────────────────