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.
- package/README.md +98 -12
- package/build/bin/cli.d.ts.map +1 -1
- package/build/bin/cli.js +70 -7
- package/build/bin/cli.js.map +1 -1
- package/build/src/dashboard-lite/render.d.ts +11 -11
- package/build/src/dashboard-lite/render.d.ts.map +1 -1
- package/build/src/dashboard-lite/render.js +40 -26
- package/build/src/dashboard-lite/render.js.map +1 -1
- package/build/src/dashboard-lite/server.d.ts +5 -6
- package/build/src/dashboard-lite/server.d.ts.map +1 -1
- package/build/src/dashboard-lite/server.js +75 -2
- package/build/src/dashboard-lite/server.js.map +1 -1
- package/build/src/pipeline-stats.d.ts +59 -0
- package/build/src/pipeline-stats.d.ts.map +1 -0
- package/build/src/pipeline-stats.js +66 -0
- package/build/src/pipeline-stats.js.map +1 -0
- package/build/src/sample-data.d.ts +49 -0
- package/build/src/sample-data.d.ts.map +1 -0
- package/build/src/sample-data.js +234 -0
- package/build/src/sample-data.js.map +1 -0
- package/build/src/schemas/career-schema.d.ts +15 -4
- package/build/src/schemas/career-schema.d.ts.map +1 -1
- package/build/src/schemas/career-schema.js +36 -2
- package/build/src/schemas/career-schema.js.map +1 -1
- package/build/src/server.d.ts +14 -1
- package/build/src/server.d.ts.map +1 -1
- package/build/src/server.js +4 -1
- package/build/src/server.js.map +1 -1
- package/build/src/storage/file-store.d.ts +10 -0
- package/build/src/storage/file-store.d.ts.map +1 -1
- package/build/src/storage/file-store.js +72 -5
- package/build/src/storage/file-store.js.map +1 -1
- package/build/src/tools/career-kb.d.ts +2 -0
- package/build/src/tools/career-kb.d.ts.map +1 -1
- package/build/src/tools/career-kb.js +71 -12
- package/build/src/tools/career-kb.js.map +1 -1
- package/build/src/tools/doctor.d.ts +115 -0
- package/build/src/tools/doctor.d.ts.map +1 -0
- package/build/src/tools/doctor.js +549 -0
- package/build/src/tools/doctor.js.map +1 -0
- package/build/src/tools/interview.d.ts.map +1 -1
- package/build/src/tools/interview.js +206 -0
- package/build/src/tools/interview.js.map +1 -1
- package/build/src/tools/opportunity.d.ts.map +1 -1
- package/build/src/tools/opportunity.js +101 -22
- package/build/src/tools/opportunity.js.map +1 -1
- package/build/src/tools/pipeline.d.ts +23 -1
- package/build/src/tools/pipeline.d.ts.map +1 -1
- package/build/src/tools/pipeline.js +110 -17
- package/build/src/tools/pipeline.js.map +1 -1
- package/build/src/types/tool-args.d.ts +4 -1
- package/build/src/types/tool-args.d.ts.map +1 -1
- package/build/src/version.d.ts +9 -0
- package/build/src/version.d.ts.map +1 -1
- package/build/src/version.js +10 -1
- package/build/src/version.js.map +1 -1
- package/data/example/career/journal.yaml +4 -4
- package/data/example/pipeline/applications.yaml +7 -7
- 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
|
-
|
|
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;
|
|
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.
|
|
83
|
-
openToRelocation: z.
|
|
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.
|
|
177
|
-
openToRelocation: z.
|
|
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;
|
|
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
|
-
|
|
79
|
-
|
|
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 ─────────────────────────────────────────────────────────────
|