careermate 0.0.1 → 0.0.2

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 (33) hide show
  1. package/INSTALL.md +12 -8
  2. package/README.md +14 -8
  3. package/dist/career-os/knowledge/interview-behavioral.md +9 -1
  4. package/dist/career-os/knowledge/offer-evaluation-decision.md +2 -2
  5. package/dist/career-os/knowledge/onboarding-first-90-days.md +1 -1
  6. package/dist/career-os/knowledge/verifiers/human-voice.md +20 -0
  7. package/dist/doctor.mjs +421 -11
  8. package/dist/mcp.mjs +421 -11
  9. package/dist/migrate.mjs +3 -1
  10. package/dist/public/index.html +1 -0
  11. package/dist/public/pages/settings.js +45 -0
  12. package/dist/public/styles.css +2 -0
  13. package/dist/site/index.html +19 -12
  14. package/dist/site/llms-install.txt +11 -8
  15. package/dist/site/start.html +16 -9
  16. package/dist/site/style.css +9 -0
  17. package/dist/web.mjs +351 -6
  18. package/docs/SUPPORTED_AI_APPS.md +6 -6
  19. package/docs/career-os/knowledge/interview-behavioral.md +9 -1
  20. package/docs/career-os/knowledge/offer-evaluation-decision.md +2 -2
  21. package/docs/career-os/knowledge/onboarding-first-90-days.md +1 -1
  22. package/docs/career-os/knowledge/verifiers/human-voice.md +20 -0
  23. package/package.json +1 -1
  24. package/packages/core/src/index.ts +1 -0
  25. package/packages/core/src/services.ts +61 -2
  26. package/packages/core/src/verify/index.ts +11 -0
  27. package/packages/core/src/verify/lexicons.ts +149 -0
  28. package/packages/core/src/verify/lint.ts +248 -0
  29. package/packages/core/src/verify/normalize.ts +112 -0
  30. package/packages/db/src/index.ts +1 -0
  31. package/packages/db/src/settings.ts +41 -0
  32. package/packages/mcp-tools/src/tools.ts +80 -6
  33. package/packages/shared/src/schemas.ts +8 -0
package/dist/mcp.mjs CHANGED
@@ -67603,8 +67603,8 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
67603
67603
  return [g, g, g];
67604
67604
  }
67605
67605
  static G_HTML([g]) {
67606
- const G = makeColorComp(g);
67607
- return `#${G}${G}${G}`;
67606
+ const G2 = makeColorComp(g);
67607
+ return `#${G2}${G2}${G2}`;
67608
67608
  }
67609
67609
  static RGB_G([r, g, b]) {
67610
67610
  return ["G", 0.3 * r + 0.59 * g + 0.11 * b];
@@ -92496,6 +92496,26 @@ function fromJson(text, fallback) {
92496
92496
  var toBit = (b) => b ? 1 : 0;
92497
92497
  var fromBit = (n) => Number(n) === 1;
92498
92498
 
92499
+ // packages/db/src/settings.ts
92500
+ var SETTING_PREFIX = "setting.";
92501
+ function getSetting(key) {
92502
+ const row = getDb().prepare(`SELECT value FROM _meta WHERE key = ?`).get(SETTING_PREFIX + key);
92503
+ return row ? row.value : null;
92504
+ }
92505
+ function setSetting(key, value) {
92506
+ getDb().prepare(
92507
+ `INSERT INTO _meta (key, value) VALUES (?, ?)
92508
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`
92509
+ ).run(SETTING_PREFIX + key, value);
92510
+ }
92511
+ function getVerifyStrict() {
92512
+ return getSetting("verify_strict") === "1";
92513
+ }
92514
+ function setVerifyStrict(on) {
92515
+ setSetting("verify_strict", on ? "1" : "0");
92516
+ return on;
92517
+ }
92518
+
92499
92519
  // packages/shared/src/enums.ts
92500
92520
  var APPLICATION_STATUSES = [
92501
92521
  "draft",
@@ -92731,7 +92751,9 @@ var CoverLetterVersionInputSchema = external_exports.object({
92731
92751
  content: reqBody,
92732
92752
  note: optNote.describe("\uC774 \uBC84\uC804\uC758 \uBCC0\uACBD \uC694\uC57D (\uC608: \uC9C0\uC6D0\uB3D9\uAE30 \uBCF4\uAC15)"),
92733
92753
  source: external_exports.enum(CONTENT_SOURCES).optional(),
92734
- set_current: external_exports.boolean().optional().describe("\uC774 \uBC84\uC804\uC744 \uD604\uC7AC \uBC84\uC804\uC73C\uB85C \uC9C0\uC815 (\uAE30\uBCF8 true)")
92754
+ set_current: external_exports.boolean().optional().describe("\uC774 \uBC84\uC804\uC744 \uD604\uC7AC \uBC84\uC804\uC73C\uB85C \uC9C0\uC815 (\uAE30\uBCF8 true)"),
92755
+ force: external_exports.boolean().optional().describe("\uC800\uC7A5 \uC804 \uC790\uB3D9 \uC810\uAC80(\uADFC\uAC70 \uC5C6\uB294 \uC218\uCE58)\uC5D0\uC11C \uB9C9\uD614\uC744 \uB54C, \uC758\uB3C4\uD55C \uAC12\uC784\uC744 \uD655\uC778\uD558\uACE0 \uADF8\uB300\uB85C \uC800\uC7A5"),
92756
+ strict: external_exports.boolean().optional().describe("\uC774\uBC88 \uC800\uC7A5\uB9CC \uC5C4\uACA9\uD558\uAC8C \uC810\uAC80(\uC774\uB825\uC11C \uBCF8\uBB38\uC5D0 \uADFC\uAC70 \uC5C6\uB294 \uC218\uCE58\uB3C4 \uCC28\uB2E8). \uC0DD\uB7B5\uD558\uBA74 \uC800\uC7A5\uB41C \uBAA8\uB4DC \uC124\uC815\uC744 \uB530\uB984")
92735
92757
  });
92736
92758
  var CoverLetterVersionRecordSchema = external_exports.object({
92737
92759
  id,
@@ -92893,7 +92915,7 @@ var ApplicationContextSchema = external_exports.object({
92893
92915
  });
92894
92916
 
92895
92917
  // packages/shared/src/build-info.ts
92896
- var APP_VERSION = true ? "0.0.1" : "0.0.0-dev";
92918
+ var APP_VERSION = true ? "0.0.2" : "0.0.0-dev";
92897
92919
  var BUNDLED = true;
92898
92920
 
92899
92921
  // packages/shared/src/index.ts
@@ -93709,7 +93731,333 @@ function getApplicationContext(opts = {}) {
93709
93731
  };
93710
93732
  }
93711
93733
 
93734
+ // packages/core/src/verify/normalize.ts
93735
+ var ZERO_WIDTH = /[​-‍⁠]/g;
93736
+ function normalizeText(s) {
93737
+ return (s ?? "").normalize("NFKC").replace(ZERO_WIDTH, "");
93738
+ }
93739
+ var MAGNITUDE = { "\uC5B5": 1e8, "\uB9CC": 1e4, "\uCC9C": 1e3 };
93740
+ var RISK_UNITS = /* @__PURE__ */ new Set([
93741
+ "%",
93742
+ "\uBC30",
93743
+ "\uC6D0",
93744
+ "\uAC74",
93745
+ "\uBA85",
93746
+ "\uC810",
93747
+ "\uC704",
93748
+ "\uB144\uCC28",
93749
+ "\uB144",
93750
+ "\uAC1C\uC6D4",
93751
+ "\uC8FC",
93752
+ "\uC2DC\uAC04",
93753
+ "\uD68C",
93754
+ "\uAC1C"
93755
+ ]);
93756
+ var NUM_RE = /(\d{1,3}(?:,\d{3})+|\d+)(?:\.(\d+))?\s*(억|만|천)?\s*(원|%p|%|%|퍼센트|배|건|명|점|위|년차|개월|년|시간|주|회|개)?/g;
93757
+ var YEAR_RE = /^(?:19|20)\d{2}$/;
93758
+ function normUnit(u) {
93759
+ if (u === "\uFF05" || u === "\uD37C\uC13C\uD2B8" || u === "%p") return "%";
93760
+ return u;
93761
+ }
93762
+ function extractNumbers(text) {
93763
+ const t = normalizeText(text);
93764
+ const out = [];
93765
+ for (const m of t.matchAll(NUM_RE)) {
93766
+ const intPart = m[1].replace(/,/g, "");
93767
+ const frac = m[2] ? "." + m[2] : "";
93768
+ const base = Number(intPart + frac);
93769
+ if (!Number.isFinite(base)) continue;
93770
+ const mag = m[3] ?? "";
93771
+ let unit = normUnit(m[4] ?? "");
93772
+ if (!mag && (!unit || unit === "\uB144") && YEAR_RE.test(intPart) && !frac) continue;
93773
+ const scale = MAGNITUDE[mag] ?? 1;
93774
+ const value = base * scale;
93775
+ if (mag && !unit) unit = "\uC6D0";
93776
+ const surface = m[0].trim();
93777
+ if (!surface) continue;
93778
+ const risk = RISK_UNITS.has(unit) || !unit && value >= 1e3 || !!mag && !unit;
93779
+ out.push({ raw: surface, value, unit, risk });
93780
+ }
93781
+ return out;
93782
+ }
93783
+ function splitSegments(text) {
93784
+ return normalizeText(text).split(/[\n.。!?!?·••]|(?:\s-\s)/).map((s) => s.trim()).filter(Boolean);
93785
+ }
93786
+ function addToMap(map2, unit, value) {
93787
+ const arr = map2.get(unit);
93788
+ if (arr) arr.push(value);
93789
+ else map2.set(unit, [value]);
93790
+ }
93791
+ function buildCorpus(text) {
93792
+ const byUnit = /* @__PURE__ */ new Map();
93793
+ const segments = [];
93794
+ for (const seg of splitSegments(text)) {
93795
+ const segMap = /* @__PURE__ */ new Map();
93796
+ for (const n of extractNumbers(seg)) {
93797
+ addToMap(byUnit, n.unit, n.value);
93798
+ addToMap(segMap, n.unit, n.value);
93799
+ }
93800
+ if (segMap.size) segments.push(segMap);
93801
+ }
93802
+ return { byUnit, segments };
93803
+ }
93804
+
93805
+ // packages/core/src/verify/lexicons.ts
93806
+ var G = (src, flags = "g") => new RegExp(src, flags);
93807
+ var GENERIC_OPENERS = {
93808
+ id: "generic_opener",
93809
+ label: "\uC81C\uB108\uB9AD\xB7\uC0C1\uD22C\uC801 \uB3C4\uC785(\uD68C\uC0AC\uBA85 \uBC14\uAFD4\uB3C4 \uB9D0\uC774 \uB418\uB294 \uC2DC\uC791)",
93810
+ regex: G(
93811
+ [
93812
+ "i am writing to apply",
93813
+ "i have always been passionate",
93814
+ "as a recent graduate",
93815
+ "to whom it may concern",
93816
+ "\uC9C0\uC6D0\uD558\uAC8C \uB418\uC5C8\uC2B5\uB2C8\uB2E4",
93817
+ "\uC9C0\uC6D0\uD569\uB2C8\uB2E4",
93818
+ "\uC5B4\uB9B4 ?\uC801\uBD80\uD130",
93819
+ "\uBCF4\uBA70 \uC790\uB790\uC2B5\uB2C8\uB2E4",
93820
+ "\uC800\uB294 .{0,10}\uD55C \uC0AC\uB78C\uC785\uB2C8\uB2E4",
93821
+ "\uD3C9\uC18C .{0,14}\uAD00\uC2EC\uC774 \uB9CE\uC544",
93822
+ "\uD56D\uC0C1 .{0,12}\uBBFF\uC5B4",
93823
+ "\uB370\uC774\uD130\uC758 \uD798\uC744 \uBBFF"
93824
+ ].join("|"),
93825
+ "gi"
93826
+ )
93827
+ };
93828
+ var EMPTY_ADJECTIVES = {
93829
+ id: "empty_adjective",
93830
+ label: "\uC99D\uAC70 \uC5C6\uB294 \uD3C9\uAC00\uD615 \uD615\uC6A9\uC0AC\xB7\uC790\uAE30\uC11C\uC220",
93831
+ regex: G(
93832
+ [
93833
+ "\uC131\uC2E4(\uD558|\uD55C|\uD568)",
93834
+ "\uCC45\uC784\uAC10\uC774 \uAC15",
93835
+ "\uC5F4\uC815(\uC744 \uAC00\uC9C0|\uC801)",
93836
+ "\uC18C\uD1B5\uC744 \uC798",
93837
+ "\uCEE4\uBBA4\uB2C8\uCF00\uC774\uC158\uC774 \uC6D0\uD65C",
93838
+ "\uAF3C\uAF3C(\uD558|\uD55C|\uD568)",
93839
+ "\uB048\uAE30(\uC788|\uAC00)",
93840
+ "\uD5CC\uC2E0\uC801",
93841
+ "\uC8FC\uB3C4\uC801\uC778",
93842
+ "\uC131\uC7A5\uD558\uB294 \uC778\uC7AC",
93843
+ "\uB77C\uACE0 \uC0DD\uAC01\uD569\uB2C8\uB2E4",
93844
+ "\uB9CE\uC740 \uAC83\uC744 \uBC30\uC6E0",
93845
+ "\uD1B5\uD574 \uC131\uC7A5",
93846
+ "\uAE30\uC5EC\uD558\uACE0 \uC2F6",
93847
+ "\uCD5C\uC120\uC744 \uB2E4",
93848
+ "\uBCF4\uD0EC\uC774 \uB418\uACA0",
93849
+ "team player",
93850
+ "hard-?working",
93851
+ "detail-?oriented",
93852
+ "motivated",
93853
+ "proactive",
93854
+ "dedicated",
93855
+ "diligent"
93856
+ ].join("|"),
93857
+ "gi"
93858
+ )
93859
+ };
93860
+ var CLICHES = {
93861
+ id: "cliche",
93862
+ label: "AI \uD074\uB9AC\uC170\xB7\uACF5\uD5C8\uD55C \uC0C1\uD22C\uC5B4",
93863
+ regex: G(
93864
+ [
93865
+ "\uAE09\uBCC0\uD558\uB294 \uC2DC\uB300",
93866
+ "4\uCC28 \uC0B0\uC5C5\uD601\uBA85",
93867
+ "\uB04A\uC784\uC5C6\uB294 \uB178\uB825",
93868
+ "\uADC0\uC0AC\uC758 \uBC1C\uC804\uC5D0 \uAE30\uC5EC",
93869
+ "\uADC0\uC0AC",
93870
+ "\uC18C\uD1B5\uACFC \uD611\uC5C5",
93871
+ "\uAE00\uB85C\uBC8C \uC778\uC7AC",
93872
+ "\uBB34\uD55C\uD55C \uAC00\uB2A5\uC131",
93873
+ "\uD55C \uB2E8\uACC4 \uB354 \uC131\uC7A5"
93874
+ ].join("|")
93875
+ )
93876
+ };
93877
+ var TRANSLATIONESE = {
93878
+ id: "translationese",
93879
+ label: "\uBC88\uC5ED\uD22C \uBB38\uBC95(\uC601\uC5B4 \uC9C1\uC5ED\uCCB4)",
93880
+ regex: G(
93881
+ [
93882
+ "\uC5D0 \uB300\uD55C",
93883
+ "\uC744 \uD1B5\uD574",
93884
+ "\uB97C \uD1B5\uD574",
93885
+ "\uC5D0 \uC788\uC5B4\uC11C",
93886
+ "\uB77C\uB294 \uC810\uC5D0\uC11C",
93887
+ "\uAC00\uC9C0\uACE0 \uC788\uC2B5\uB2C8\uB2E4",
93888
+ "\uB418\uC5B4\uC84C",
93889
+ "\uB418\uC5B4\uC9D1\uB2C8\uB2E4",
93890
+ "\uD568\uC73C\uB85C\uC368",
93891
+ "\uC5D0 \uB2E4\uB984 \uC544\uB2C8",
93892
+ "\uC9C4\uD589\uD558\uC600\uC2B5\uB2C8\uB2E4"
93893
+ ].join("|")
93894
+ )
93895
+ };
93896
+ var HYPE = {
93897
+ id: "hype",
93898
+ label: "\uACFC\uC7A5 \uC218\uC2DD\uC5B4",
93899
+ regex: G(["\uB9E4\uC6B0", "\uC815\uB9D0\uB85C", "\uD0C1\uC6D4(\uD55C|\uD558\uAC8C)", "\uC131\uACF5\uC801\uC73C\uB85C", "\uC644\uBCBD(\uD55C|\uD558\uAC8C)", "\uC5C4\uCCAD\uB09C", "\uD68D\uAE30\uC801"].join("|"))
93900
+ };
93901
+ var BLIND_PII = {
93902
+ id: "blind_pii",
93903
+ label: "\uBE14\uB77C\uC778\uB4DC \uCC44\uC6A9 \uC704\uBC18 \uC18C\uC9C0(\uC9C1\uBB34\uBB34\uAD00 \uAC1C\uC778\uC815\uBCF4)",
93904
+ regex: G(["\\d{2,3}\\s?\uC138", "\uC0AC\uC9C4\\s?\uCCA8\uBD80", "\uAC00\uC871\uAD00\uACC4", "\uBCF8\uC801", "\uCD9C\uC2E0\uC9C0", "\uC0DD\uB144\uC6D4\uC77C"].join("|"))
93905
+ };
93906
+ var STYLE_LEXICONS = [
93907
+ GENERIC_OPENERS,
93908
+ EMPTY_ADJECTIVES,
93909
+ CLICHES,
93910
+ TRANSLATIONESE,
93911
+ HYPE,
93912
+ BLIND_PII
93913
+ ];
93914
+ function countHits(lex, text, maxHits = 8) {
93915
+ const hits = [];
93916
+ let count = 0;
93917
+ for (const m of text.matchAll(lex.regex)) {
93918
+ count++;
93919
+ if (hits.length < maxHits) hits.push(m[0].trim());
93920
+ }
93921
+ return { count, hits };
93922
+ }
93923
+
93924
+ // packages/core/src/verify/lint.ts
93925
+ var within = (a, b) => Math.abs(a - b) <= Math.max(0.5, 0.02 * Math.max(Math.abs(a), Math.abs(b)));
93926
+ var approx = (a, b) => Math.abs(a - b) <= Math.max(0.5, 0.01 * Math.max(Math.abs(a), Math.abs(b)));
93927
+ function isExplained(token, corpus) {
93928
+ const v = token.value;
93929
+ for (const s of corpus.byUnit.get(token.unit) ?? []) if (within(v, s)) return true;
93930
+ for (const seg of corpus.segments) {
93931
+ const vals = seg.get(token.unit);
93932
+ if (vals && vals.length >= 2) {
93933
+ for (let i2 = 0; i2 < vals.length; i2++)
93934
+ for (let j = 0; j < vals.length; j++) {
93935
+ if (i2 === j) continue;
93936
+ if (approx(v, vals[i2] - vals[j]) || approx(v, vals[i2] + vals[j])) return true;
93937
+ }
93938
+ }
93939
+ }
93940
+ if (token.unit === "%") {
93941
+ for (const seg of corpus.segments) {
93942
+ for (const [u, vals] of seg) {
93943
+ if (u === "%" || vals.length < 2) continue;
93944
+ for (let i2 = 0; i2 < vals.length; i2++)
93945
+ for (let j = 0; j < vals.length; j++) {
93946
+ if (i2 === j) continue;
93947
+ const a = vals[i2];
93948
+ const b = vals[j];
93949
+ if (b !== 0 && (approx(v, a / b * 100) || approx(v, (a - b) / b * 100) || approx(v, (a - b) / a * 100)))
93950
+ return true;
93951
+ if (a + b !== 0 && approx(v, a / (a + b) * 100)) return true;
93952
+ }
93953
+ }
93954
+ }
93955
+ }
93956
+ return false;
93957
+ }
93958
+ function analyzeProvenance(outputText, corpus) {
93959
+ const documents = buildCorpus(corpus.documents);
93960
+ const structured = buildCorpus(corpus.structured);
93961
+ const job = buildCorpus(corpus.job);
93962
+ const nums = extractNumbers(outputText).filter((n) => n.risk);
93963
+ const res = { checked: nums.length, supported: 0, fabricated: [], unverified: [], jobSourced: [] };
93964
+ for (const n of nums) {
93965
+ const tok = { raw: n.raw, value: n.value, unit: n.unit };
93966
+ if (isExplained(n, documents)) res.supported++;
93967
+ else if (isExplained(n, structured)) res.unverified.push({ ...tok, reason: "structured_only" });
93968
+ else if (isExplained(n, job)) res.jobSourced.push({ ...tok, reason: "job_only" });
93969
+ else res.fabricated.push({ ...tok, reason: "fabricated" });
93970
+ }
93971
+ return res;
93972
+ }
93973
+ function styleSignals(text) {
93974
+ const t = normalizeText(text);
93975
+ const signals = [];
93976
+ for (const lex of STYLE_LEXICONS) {
93977
+ const { count, hits } = countHits(lex, t);
93978
+ if (count > 0) {
93979
+ const severity = lex.id === "blind_pii" || lex.id === "generic_opener" ? "warn" : "info";
93980
+ signals.push({ id: lex.id, label: lex.label, severity, count, hits });
93981
+ }
93982
+ }
93983
+ return signals;
93984
+ }
93985
+ var DISCLAIMER = "\uC774 \uC810\uAC80\uC740 LLM \uC5C6\uC774 \uC140 \uC218 \uC788\uB294 \uAC83\uB9CC \uBD05\uB2C8\uB2E4(\uC22B\uC790 \uCD9C\uCC98\xB7\uBB38\uCCB4 \uC2E0\uD638). \uD1B5\uACFC=\uD569\uACA9\uC774 \uC544\uB2C8\uBA70, \uC0AC\uC2E4\xB7\uB3D9\uBB38\uC11C\uB2F5\xB7\uC758\uBBF8 \uD310\uB2E8\uC740 \uB2F9\uC2E0(AI)\uC774 \uC9C1\uC811 \uD574\uC57C \uD569\uB2C8\uB2E4.";
93986
+ function lintArtifact(kind, text, corpus, opts = {}) {
93987
+ const norm = normalizeText(text);
93988
+ const corpusAvailable = (corpus.documents + corpus.structured).trim().length > 0;
93989
+ const provenance = analyzeProvenance(text, corpus);
93990
+ const signals = styleSignals(text);
93991
+ const strict = !!opts.strict;
93992
+ const blocking = [];
93993
+ if (corpusAvailable && kind === "cover_letter") {
93994
+ if (provenance.fabricated.length > 0) {
93995
+ blocking.push({
93996
+ id: "fabricated_numbers",
93997
+ label: "\uADFC\uAC70 \uC5C6\uB294 \uC218\uCE58(\uC800\uC7A5\uB41C \uACBD\uB825\xB7\uC774\uB825\uC11C\uC5D0 \uC5C6\uC74C)",
93998
+ detail: provenance.fabricated.map((f) => f.raw).join(", ")
93999
+ });
94000
+ }
94001
+ if (strict && provenance.unverified.length > 0) {
94002
+ blocking.push({
94003
+ id: "unverified_numbers_strict",
94004
+ label: "\uC5C4\uACA9 \uBAA8\uB4DC: \uC774\uB825\uC11C \uBCF8\uBB38\uC5D0 \uADFC\uAC70 \uC5C6\uB294 \uC218\uCE58(\uAD6C\uC870\uD654 \uACBD\uB825/\uD504\uB85C\uC81D\uD2B8 \uD56D\uBAA9\uC5D0\uB9CC \uC788\uC74C)",
94005
+ detail: provenance.unverified.map((u) => u.raw).join(", ")
94006
+ });
94007
+ }
94008
+ }
94009
+ return {
94010
+ kind,
94011
+ charCount: [...norm].length,
94012
+ signals,
94013
+ provenance,
94014
+ blocking,
94015
+ corpusAvailable,
94016
+ strict,
94017
+ disclaimer: DISCLAIMER
94018
+ };
94019
+ }
94020
+ function summarizeReport(r) {
94021
+ const parts = [];
94022
+ if (r.blocking.length)
94023
+ parts.push(`\u26D4 \uC800\uC7A5 \uCC28\uB2E8: ${r.blocking.map((b) => `${b.label}(${b.detail})`).join("; ")}`);
94024
+ if (r.provenance.unverified.length)
94025
+ parts.push(
94026
+ `\uBBF8\uD655\uC778 \uC218\uCE58 ${r.provenance.unverified.length}\uAC1C(${r.provenance.unverified.map((u) => u.raw).join(", ")}) \u2014 \uC774\uB825\uC11C \uBCF8\uBB38\uC5D4 \uC5C6\uACE0 \uAD6C\uC870\uD654 \uACBD\uB825/\uD504\uB85C\uC81D\uD2B8 \uD56D\uBAA9\uC5D0\uB9CC \uC788\uC74C, \uC0AC\uC2E4\uC778\uC9C0 \uD655\uC778`
94027
+ );
94028
+ if (r.provenance.jobSourced.length)
94029
+ parts.push(`\uACF5\uACE0 \uCD9C\uCC98 \uC218\uCE58 ${r.provenance.jobSourced.length}\uAC1C(\uBCF8\uC778 \uC131\uACFC\uB85C \uC4F0\uC9C0 \uC54A\uC558\uB294\uC9C0 \uD655\uC778)`);
94030
+ const adv = r.signals.filter((s) => s.count > 0);
94031
+ if (adv.length) parts.push("\uBB38\uCCB4 \uC2E0\uD638: " + adv.map((s) => `${s.label.split("(")[0]}\xD7${s.count}`).join(", "));
94032
+ if (!parts.length) parts.push("\uC790\uB3D9 \uC810\uAC80 \uD1B5\uACFC(\uC140 \uC218 \uC788\uB294 \uD56D\uBAA9 \uAE30\uC900). \uC758\uBBF8 \uD310\uB2E8\uC740 \uC9C1\uC811 \uD655\uC778\uD558\uC138\uC694.");
94033
+ return parts.join(" \xB7 ");
94034
+ }
94035
+
93712
94036
  // packages/core/src/services.ts
94037
+ function collectVerifyCorpus(jobId) {
94038
+ const documents = [];
94039
+ for (const d of documentRepo.list()) {
94040
+ if (d.source !== "ai") documents.push(d.content);
94041
+ }
94042
+ const structured = [];
94043
+ for (const e of experienceRepo.list()) {
94044
+ structured.push([e.company, e.role, e.description, ...e.achievements ?? [], ...e.tech ?? []].filter(Boolean).join("\n"));
94045
+ }
94046
+ for (const p of projectRepo.list()) {
94047
+ structured.push([p.name, p.description, ...p.highlights ?? [], ...p.tech ?? []].filter(Boolean).join("\n"));
94048
+ }
94049
+ for (const s of skillRepo.list()) {
94050
+ structured.push([s.name, s.level, s.years != null ? `${s.years}\uB144` : ""].filter(Boolean).join(" "));
94051
+ }
94052
+ const job = jobId ? jobRepo.get(jobId) : null;
94053
+ const jobText = job ? [job.description, ...job.requirements ?? [], ...job.keywords ?? []].filter(Boolean).join("\n") : "";
94054
+ return { documents: documents.join("\n"), structured: structured.join("\n"), job: jobText };
94055
+ }
94056
+ function previewCoverLetter(text, jobId, strict) {
94057
+ return lintArtifact("cover_letter", text, collectVerifyCorpus(jobId), {
94058
+ strict: strict ?? getVerifyStrict()
94059
+ });
94060
+ }
93713
94061
  function saveProfile(input) {
93714
94062
  const p = profileRepo.save(input);
93715
94063
  activityRepo.log("profile_updated", `\uD504\uB85C\uD544\uC744 ${p.name ? `(${p.name}) ` : ""}\uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4.`, "profile", p.id);
@@ -93751,6 +94099,15 @@ function saveFitAnalysis(input) {
93751
94099
  return fit;
93752
94100
  }
93753
94101
  function saveCoverLetterVersion(input) {
94102
+ const strict = input.strict ?? getVerifyStrict();
94103
+ const verification = lintArtifact("cover_letter", input.content, collectVerifyCorpus(input.job_id), { strict });
94104
+ const forced = verification.blocking.length > 0 && input.force === true;
94105
+ if (verification.blocking.length > 0 && !forced) {
94106
+ const detail = verification.blocking.map((b) => `${b.label}: ${b.detail}`).join("; ");
94107
+ const strictHit = verification.blocking.some((b) => b.id === "unverified_numbers_strict");
94108
+ const tail = strictHit ? `\uC5C4\uACA9 \uBAA8\uB4DC\uAC00 \uCF1C\uC838 \uC788\uC5B4, \uC62C\uB9B0 \uC774\uB825\uC11C \uBCF8\uBB38\uC5D0 \uC5C6\uB294 \uC218\uCE58\uB294 \uB9C9\uD799\uB2C8\uB2E4. \uC774\uB825\uC11C\uB97C \uC62C\uB9AC\uAC70\uB098 \uC5C4\uACA9 \uBAA8\uB4DC\uB97C \uB044\uAC70\uB098, \uC758\uB3C4\uD55C \uAC12\uC774\uBA74 force: true\uB85C \uC800\uC7A5\uD558\uC138\uC694.` : `\uC774 \uC218\uCE58\uB294 \uC800\uC7A5\uB41C \uACBD\uB825\xB7\uC774\uB825\uC11C\xB7\uD504\uB85C\uC81D\uD2B8\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4. \uC6D0\uBCF8 \uB370\uC774\uD130\uC758 \uC2E4\uC81C \uC218\uCE58\uB85C \uACE0\uCE58\uAC70\uB098, \uC758\uB3C4\uD55C \uAC12\uC774 \uB9DE\uB2E4\uBA74 \uBA3C\uC800 \uACBD\uB825/\uD504\uB85C\uC81D\uD2B8\uC5D0 \uADF8 \uC218\uCE58\uB97C \uCD94\uAC00\uD55C \uB4A4 \uB2E4\uC2DC \uC800\uC7A5\uD558\uC138\uC694(\uADF8\uB300\uB85C \uC800\uC7A5\uD558\uB824\uBA74 force: true).`;
94109
+ throw new Error(`\uC800\uC7A5 \uC804 \uC790\uB3D9 \uC810\uAC80\uC5D0\uC11C \uB9C9\uD614\uC2B5\uB2C8\uB2E4 \u2014 ${detail}. ${tail}`);
94110
+ }
93754
94111
  const { coverLetter, version: version3 } = coverLetterRepo.addVersion({
93755
94112
  cover_letter_id: input.cover_letter_id,
93756
94113
  title: input.title,
@@ -93772,7 +94129,7 @@ function saveCoverLetterVersion(input) {
93772
94129
  "cover_letter",
93773
94130
  coverLetter.id
93774
94131
  );
93775
- return { coverLetter, version: version3 };
94132
+ return { coverLetter, version: version3, verification, forced };
93776
94133
  }
93777
94134
  function updateApplicationStatus(jobId, status, note) {
93778
94135
  const job = jobRepo.get(jobId);
@@ -95625,16 +95982,22 @@ ${summary}
95625
95982
  const s = getOnboardingStatus();
95626
95983
  const lines = s.next_steps.map((t, i2) => `${i2 + 1}. ${t}`).join("\n");
95627
95984
  const upd = getUpdateStatus();
95985
+ const strict = getVerifyStrict();
95628
95986
  let text = `\uC628\uBCF4\uB529 ${s.completed ? "\uC644\uB8CC\uB428" : "\uC9C4\uD589 \uD544\uC694"} \xB7 \uD504\uB85C\uD544 \uC644\uC131\uB3C4 ${s.profile_completeness}%
95629
95987
 
95630
95988
  \uB2E4\uC74C \uB2E8\uACC4:
95631
95989
  ${lines}`;
95990
+ if (!s.completed) {
95991
+ text += `
95992
+
95993
+ \u{1F4A1} \uCC38\uACE0: \uC790\uC18C\uC11C\uB97C \uC800\uC7A5\uD560 \uB54C CareerMate\uAC00 \uBCF8\uBB38\uC758 \uC218\uCE58\uB97C \uC800\uC7A5\uB41C \uACBD\uB825\xB7\uC774\uB825\uC11C\uC640 \uC790\uB3D9 \uB300\uC870\uD574, \uADFC\uAC70 \uC5C6\uB294 \uC218\uCE58\uB294 \uB9C9\uC2B5\uB2C8\uB2E4. \uB354 \uAE50\uAE50\uD558\uAC8C \uBCF4\uAE38 \uC6D0\uD558\uBA74 "\uC5C4\uACA9\uD558\uAC8C \uBD10\uC918"\uB77C\uACE0 \uD558\uAC70\uB098 \uB300\uC2DC\uBCF4\uB4DC \uC124\uC815\uC5D0\uC11C \uC5C4\uACA9 \uBAA8\uB4DC\uB97C \uCF24 \uC218 \uC788\uC5B4\uC694(\uC774\uB825\uC11C \uBCF8\uBB38\uC5D0 \uC5C6\uB294 \uC218\uCE58\uAE4C\uC9C0 \uCC28\uB2E8).`;
95994
+ }
95632
95995
  if (upd.update_available) {
95633
95996
  text += `
95634
95997
 
95635
95998
  \u{1F195} \uC0C8 \uBC84\uC804 v${upd.latest} \uC0AC\uC6A9 \uAC00\uB2A5 (\uD604\uC7AC v${upd.current}). \uC0AC\uC6A9\uC790\uAC00 \uC6D0\uD558\uBA74 update_careermate \uB3C4\uAD6C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.`;
95636
95999
  }
95637
- return ok(text, { ...s, update: upd });
96000
+ return ok(text, { ...s, verify_strict: strict, update: upd });
95638
96001
  }
95639
96002
  },
95640
96003
  /* --------------------------------------------------------------- profile */
@@ -95768,11 +96131,20 @@ ${lines}`;
95768
96131
  {
95769
96132
  name: "save_cover_letter_version",
95770
96133
  title: "\uC790\uAE30\uC18C\uAC1C\uC11C \uBC84\uC804 \uC800\uC7A5",
95771
- description: '\uC790\uAE30\uC18C\uAC1C\uC11C\uC758 \uC0C8 \uBC84\uC804\uC744 \uC800\uC7A5\uD569\uB2C8\uB2E4. CareerMate \uC790\uC18C\uC11C \uC791\uC131 \uC6CC\uD06C\uD50C\uB85C\uC6B0\uC758 \uD575\uC2EC \uC800\uC7A5 \uB2E8\uACC4\uC785\uB2C8\uB2E4. cover_letter_id\uB97C \uC8FC\uBA74 \uAE30\uC874 \uC790\uC18C\uC11C\uC5D0 \uC0C8 \uBC84\uC804\uC744 \uCD94\uAC00\uD558\uACE0, \uC5C6\uC73C\uBA74 \uC0C8 \uC790\uC18C\uC11C\uB97C \uB9CC\uB4E4\uC5B4 v1\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4(\uC774\uB54C title \uAD8C\uC7A5). note\uC5D0 \uC774 \uBC84\uC804\uC758 \uBCC0\uACBD \uC694\uC57D(\uC608: "\uC9C0\uC6D0\uB3D9\uAE30 \uBCF4\uAC15")\uC744 \uB0A8\uAE30\uBA74 \uC0AC\uC6A9\uC790\uAC00 \uB300\uC2DC\uBCF4\uB4DC\uC5D0\uC11C \uBC84\uC804 \uD788\uC2A4\uD1A0\uB9AC\uB97C \uC774\uD574\uD558\uAE30 \uC27D\uC2B5\uB2C8\uB2E4. job_id\uB85C \uACF5\uACE0\uC5D0 \uC5F0\uACB0\uD558\uBA74 \uC9C0\uC6D0 \uD56D\uBAA9\uACFC \uC790\uB3D9 \uC5F0\uACB0\uB429\uB2C8\uB2E4.',
96134
+ description: '\uC790\uAE30\uC18C\uAC1C\uC11C\uC758 \uC0C8 \uBC84\uC804\uC744 \uC800\uC7A5\uD569\uB2C8\uB2E4. CareerMate \uC790\uC18C\uC11C \uC791\uC131 \uC6CC\uD06C\uD50C\uB85C\uC6B0\uC758 \uD575\uC2EC \uC800\uC7A5 \uB2E8\uACC4\uC785\uB2C8\uB2E4. cover_letter_id\uB97C \uC8FC\uBA74 \uAE30\uC874 \uC790\uC18C\uC11C\uC5D0 \uC0C8 \uBC84\uC804\uC744 \uCD94\uAC00\uD558\uACE0, \uC5C6\uC73C\uBA74 \uC0C8 \uC790\uC18C\uC11C\uB97C \uB9CC\uB4E4\uC5B4 v1\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4(\uC774\uB54C title \uAD8C\uC7A5). note\uC5D0 \uC774 \uBC84\uC804\uC758 \uBCC0\uACBD \uC694\uC57D(\uC608: "\uC9C0\uC6D0\uB3D9\uAE30 \uBCF4\uAC15")\uC744 \uB0A8\uAE30\uBA74 \uC0AC\uC6A9\uC790\uAC00 \uB300\uC2DC\uBCF4\uB4DC\uC5D0\uC11C \uBC84\uC804 \uD788\uC2A4\uD1A0\uB9AC\uB97C \uC774\uD574\uD558\uAE30 \uC27D\uC2B5\uB2C8\uB2E4. job_id\uB85C \uACF5\uACE0\uC5D0 \uC5F0\uACB0\uD558\uBA74 \uC9C0\uC6D0 \uD56D\uBAA9\uACFC \uC790\uB3D9 \uC5F0\uACB0\uB429\uB2C8\uB2E4. \uC800\uC7A5 \uC9C1\uC804 \uC790\uB3D9\uC73C\uB85C "\uADFC\uAC70 \uC5C6\uB294 \uC218\uCE58" \uC810\uAC80\uC744 \uB3CC\uB9BD\uB2C8\uB2E4 \u2014 \uBCF8\uBB38\uC758 \uC815\uB7C9 \uC218\uCE58\uAC00 \uC800\uC7A5\uB41C \uACBD\uB825\xB7\uC774\uB825\uC11C\xB7\uD504\uB85C\uC81D\uD2B8\uC5D0 \uADFC\uAC70\uAC00 \uC5C6\uC73C\uBA74(\uD658\uAC01 \uC758\uC2EC) \uC800\uC7A5\uC774 \uB9C9\uD799\uB2C8\uB2E4. \uADF8\uB54C\uB294 \uC6D0\uBCF8\uC758 \uC2E4\uC81C \uC218\uCE58\uB85C \uACE0\uCE58\uAC70\uB098, \uC758\uB3C4\uD55C \uAC12\uC774\uBA74 force:true\uB85C \uB2E4\uC2DC \uC800\uC7A5\uD558\uC138\uC694.',
95772
96135
  inputSchema: CoverLetterVersionInputSchema.shape,
95773
96136
  handler: (args) => {
95774
- const { coverLetter, version: version3 } = saveCoverLetterVersion(args);
95775
- return ok(`'${coverLetter.title}' v${version3.version_no}\uC744 \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4.`, { coverLetter, version: version3 });
96137
+ try {
96138
+ const { coverLetter, version: version3, verification, forced } = saveCoverLetterVersion(args);
96139
+ const advisory = summarizeReport(verification);
96140
+ const head = `'${coverLetter.title}' v${version3.version_no}\uC744 \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4.`;
96141
+ const hasAdvisory = verification.signals.length || verification.provenance.jobSourced.length || verification.provenance.unverified.length;
96142
+ const note = forced ? "\n\u26A0\uFE0F \uADFC\uAC70 \uC5C6\uB294 \uC218\uCE58\uAC00 \uC788\uB294\uB370\uB3C4 force\uB85C \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4(\uAC80\uC99D \uBBF8\uD1B5\uACFC\uB85C \uAE30\uB85D). \uBA74\uC811 \uC804 \uC0AC\uC2E4 \uD655\uC778\uC744 \uAD8C\uD569\uB2C8\uB2E4." : hasAdvisory ? `
96143
+ \uC810\uAC80: ${advisory}` : "";
96144
+ return ok(head + note, { coverLetter, version: version3, verification });
96145
+ } catch (e) {
96146
+ return fail(e instanceof Error ? e.message : "\uC800\uC7A5 \uC2E4\uD328");
96147
+ }
95776
96148
  }
95777
96149
  },
95778
96150
  /* ------------------------------------------------------------------ jobs */
@@ -96002,7 +96374,7 @@ ${lines}`;
96002
96374
  handler: (args) => {
96003
96375
  try {
96004
96376
  const content = getPlaybook(args.domain);
96005
- return ok(content, { domain: args.domain, content });
96377
+ return ok(content, { domain: args.domain });
96006
96378
  } catch (e) {
96007
96379
  return fail(e instanceof Error ? e.message : "\uD50C\uB808\uC774\uBD81\uC744 \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.");
96008
96380
  }
@@ -96019,12 +96391,50 @@ ${lines}`;
96019
96391
  handler: (args) => {
96020
96392
  try {
96021
96393
  const content = getVerifier(args.id);
96022
- return ok(content, { id: args.id, content });
96394
+ return ok(content, { id: args.id });
96023
96395
  } catch (e) {
96024
96396
  return fail(e instanceof Error ? e.message : "\uAC80\uC99D \uB8E8\uBE0C\uB9AD\uC744 \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.");
96025
96397
  }
96026
96398
  }
96027
96399
  },
96400
+ /* ----------------------------------------- career-os: deterministic lint */
96401
+ {
96402
+ name: "validate_cover_letter",
96403
+ title: "\uC790\uAE30\uC18C\uAC1C\uC11C \uC790\uB3D9 \uC810\uAC80 (\uC800\uC7A5 \uC804 \uBBF8\uB9AC\uBCF4\uAE30)",
96404
+ description: "\uC790\uAE30\uC18C\uAC1C\uC11C\uB97C save_cover_letter_version\uC73C\uB85C \uC800\uC7A5\uD558\uAE30 \uC9C1\uC804\uC5D0 \uD638\uCD9C\uD558\uBA74, \uC800\uC7A5\uD558\uC9C0 \uC54A\uACE0 \uBBF8\uB9AC \uC810\uAC80\uB9CC \uD569\uB2C8\uB2E4. CareerMate\uAC00 LLM \uC5C6\uC774 \uACB0\uC815\uB860\uC73C\uB85C (1) \uBCF8\uBB38\uC758 \uC815\uB7C9 \uC218\uCE58\uAC00 \uC800\uC7A5\uB41C \uACBD\uB825\xB7\uC774\uB825\uC11C\xB7\uD504\uB85C\uC81D\uD2B8\uC5D0 \uADFC\uAC70\uAC00 \uC788\uB294\uC9C0(\uC5C6\uC73C\uBA74 \uD658\uAC01 \uC758\uC2EC \u2192 \uC800\uC7A5 \uC2DC \uCC28\uB2E8\uB428), (2) \uACF5\uACE0\uC5D0\uC11C \uAC00\uC838\uC628 \uC218\uCE58\uB97C \uBCF8\uC778 \uC131\uACFC\uCC98\uB7FC \uC4F0\uC9C0 \uC54A\uC558\uB294\uC9C0, (3) \uBC88\uC5ED\uD22C\xB7\uC81C\uB108\uB9AD \uB3C4\uC785\xB7\uBE48 \uD615\uC6A9\uC0AC\xB7\uBE14\uB77C\uC778\uB4DC \uC704\uBC18 \uAC19\uC740 \uBB38\uCCB4 \uC2E0\uD638\uB97C \uC138\uC5B4 \uB3CC\uB824\uC90D\uB2C8\uB2E4. \uC810\uAC80\uC740 \uC140 \uC218 \uC788\uB294 \uAC83\uB9CC \uBD05\uB2C8\uB2E4 \u2014 \uC0AC\uC2E4\uC131\xB7\uB3D9\uBB38\uC11C\uB2F5 \uAC19\uC740 \uC758\uBBF8 \uD310\uB2E8\uC740 \uB2F9\uC2E0(AI)\uC774 \uC9C1\uC811 \uD558\uC138\uC694. job_id\uB97C \uC8FC\uBA74 \uACF5\uACE0 \uC218\uCE58\uAE4C\uC9C0 \uBE44\uAD50\uD569\uB2C8\uB2E4.",
96405
+ inputSchema: {
96406
+ text: external_exports.string().max(2e5).describe("\uC810\uAC80\uD560 \uC790\uAE30\uC18C\uAC1C\uC11C \uBCF8\uBB38"),
96407
+ job_id: external_exports.string().optional().describe("\uC5F0\uACB0\uD560 \uACF5\uACE0(\uC788\uC73C\uBA74 \uACF5\uACE0 \uC218\uCE58\uAE4C\uC9C0 \uBE44\uAD50)"),
96408
+ strict: external_exports.boolean().optional().describe('\uC774\uBC88 \uC810\uAC80\uB9CC \uC5C4\uACA9\uD558\uAC8C(\uC774\uB825\uC11C \uBCF8\uBB38\uC5D0 \uADFC\uAC70 \uC5C6\uB294 \uC218\uCE58\uB3C4 \uCC28\uB2E8). \uC0AC\uC6A9\uC790\uAC00 "\uC5C4\uACA9\uD558\uAC8C \uBD10\uC918"\uB77C\uACE0 \uD560 \uB54C true. \uC0DD\uB7B5\uD558\uBA74 \uC800\uC7A5\uB41C \uBAA8\uB4DC\uB97C \uB530\uB984')
96409
+ },
96410
+ readOnly: true,
96411
+ handler: (args) => {
96412
+ const text = typeof args?.text === "string" ? args.text : "";
96413
+ if (!text.trim()) return fail("\uC810\uAC80\uD560 \uBCF8\uBB38\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.");
96414
+ const report = previewCoverLetter(text, args?.job_id ?? null, args?.strict);
96415
+ const verdict = report.blocking.length ? `\u26D4 \uC774\uB300\uB85C \uC800\uC7A5\uD558\uBA74 \uB9C9\uD799\uB2C8\uB2E4 \u2014 ${report.blocking.map((b) => `${b.label}: ${b.detail}`).join("; ")}` : "\u2705 \uC790\uB3D9 \uCC28\uB2E8 \uD56D\uBAA9 \uC5C6\uC74C(\uC140 \uC218 \uC788\uB294 \uD56D\uBAA9 \uAE30\uC900).";
96416
+ const modeLine = report.strict ? "\n(\uC5C4\uACA9 \uBAA8\uB4DC \uC801\uC6A9 \uC911)" : "";
96417
+ return ok(`${verdict}${modeLine}
96418
+ ${summarizeReport(report)}
96419
+
96420
+ ${report.disclaimer}`, report);
96421
+ }
96422
+ },
96423
+ {
96424
+ name: "set_verify_mode",
96425
+ title: "\uC810\uAC80 \uBAA8\uB4DC \uC124\uC815 (\uAE30\uBCF8/\uC5C4\uACA9)",
96426
+ description: '\uC800\uC7A5 \uC804 \uC790\uB3D9 \uC810\uAC80\uC758 \uC5C4\uACA9\uB3C4\uB97C \uBC14\uAFC9\uB2C8\uB2E4. \uC5C4\uACA9 \uBAA8\uB4DC(strict=true)\uB97C \uCF1C\uBA74, \uC774\uD6C4 \uBAA8\uB4E0 \uC790\uC18C\uC11C \uC800\uC7A5\uC5D0\uC11C "\uC774\uB825\uC11C \uBCF8\uBB38\uC5D0 \uADFC\uAC70 \uC5C6\uB294 \uC218\uCE58"(\uAD6C\uC870\uD654 \uACBD\uB825 \uD56D\uBAA9\uC5D0\uB9CC \uC788\uB294 \uAC12)\uAE4C\uC9C0 \uCC28\uB2E8\uD569\uB2C8\uB2E4. \uAE30\uBCF8 \uBAA8\uB4DC\uB294 \uCD9C\uCC98\uAC00 \uC544\uC608 \uC5C6\uB294 \uC218\uCE58\uB9CC \uCC28\uB2E8\uD569\uB2C8\uB2E4. \uC0AC\uC6A9\uC790\uAC00 "\uC5C4\uACA9\uD558\uAC8C \uBD10\uC918"\uB77C\uACE0 \uD558\uBA74 \uBC14\uB85C \uCF1C\uC9C0 \uB9D0\uACE0 "\uC55E\uC73C\uB85C \uD56D\uC0C1 \uC5C4\uACA9\uD558\uAC8C \uBCFC\uAE4C\uC694?"\uB77C\uACE0 \uBA3C\uC800 \uD655\uC778\uD55C \uB4A4 \uB3D9\uC758\uD558\uBA74 strict=true\uB85C \uD638\uCD9C\uD558\uC138\uC694. \uB044\uAE30(strict=false)\uB294 \uC548\uC804\uC7A5\uCE58\uB97C \uC57D\uD558\uAC8C \uB9CC\uB4DC\uB294 \uAC83\uC774\uBBC0\uB85C, \uC0AC\uC6A9\uC790\uAC00 \uBA85\uD655\uD788 \uB044\uACA0\uB2E4\uACE0 \uD560 \uB54C\uB9CC \uD638\uCD9C\uD558\uC138\uC694. \uC774\uBC88 \uD55C \uAC74\uB9CC \uC5C4\uACA9\uD558\uAC8C \uBCF4\uB824\uBA74 \uBAA8\uB4DC\uB97C \uBC14\uAFB8\uC9C0 \uB9D0\uACE0 validate_cover_letter / save_cover_letter_version\uC5D0 strict:true\uB97C \uD55C \uBC88\uB9CC \uB118\uAE30\uC138\uC694.',
96427
+ inputSchema: {
96428
+ strict: external_exports.boolean().describe("true=\uC5C4\uACA9 \uCF1C\uAE30, false=\uB044\uAE30(\uBA85\uD655\uD55C \uC0AC\uC6A9\uC790 \uC758\uC0AC\uC77C \uB54C\uB9CC)")
96429
+ },
96430
+ handler: (args) => {
96431
+ const on = setVerifyStrict(!!args?.strict);
96432
+ return ok(
96433
+ on ? '\uC5C4\uACA9 \uC810\uAC80 \uBAA8\uB4DC\uB97C \uCF30\uC2B5\uB2C8\uB2E4. \uC774\uC81C \uC790\uC18C\uC11C\uB97C \uC800\uC7A5\uD560 \uB54C, \uC62C\uB9B0 \uC774\uB825\uC11C \uBCF8\uBB38\uC5D0 \uADFC\uAC70\uAC00 \uC5C6\uB294 \uC218\uCE58\uB294 \uB9C9\uD799\uB2C8\uB2E4(\uAD6C\uC870\uD654 \uACBD\uB825 \uD56D\uBAA9\uC5D0\uB9CC \uC788\uB294 \uAC12 \uD3EC\uD568). \uB044\uB824\uBA74 "\uC5C4\uACA9 \uBAA8\uB4DC \uAEBC\uC918"\uB77C\uACE0 \uD558\uC138\uC694.' : "\uC5C4\uACA9 \uC810\uAC80 \uBAA8\uB4DC\uB97C \uAED0\uC2B5\uB2C8\uB2E4. \uC774\uC81C \uCD9C\uCC98\uAC00 \uC544\uC608 \uC5C6\uB294 \uC218\uCE58\uB9CC \uCC28\uB2E8\uD569\uB2C8\uB2E4(\uAD6C\uC870\uD654 \uD56D\uBAA9\uC5D0\uB9CC \uC788\uB294 \uAC12\uC740 \uACBD\uACE0\uB9CC).",
96434
+ { strict: on }
96435
+ );
96436
+ }
96437
+ },
96028
96438
  /* --------------------------------------------------- writing style guide */
96029
96439
  {
96030
96440
  name: "get_writing_style_guide",
package/dist/migrate.mjs CHANGED
@@ -4568,7 +4568,9 @@ var CoverLetterVersionInputSchema = external_exports.object({
4568
4568
  content: reqBody,
4569
4569
  note: optNote.describe("\uC774 \uBC84\uC804\uC758 \uBCC0\uACBD \uC694\uC57D (\uC608: \uC9C0\uC6D0\uB3D9\uAE30 \uBCF4\uAC15)"),
4570
4570
  source: external_exports.enum(CONTENT_SOURCES).optional(),
4571
- set_current: external_exports.boolean().optional().describe("\uC774 \uBC84\uC804\uC744 \uD604\uC7AC \uBC84\uC804\uC73C\uB85C \uC9C0\uC815 (\uAE30\uBCF8 true)")
4571
+ set_current: external_exports.boolean().optional().describe("\uC774 \uBC84\uC804\uC744 \uD604\uC7AC \uBC84\uC804\uC73C\uB85C \uC9C0\uC815 (\uAE30\uBCF8 true)"),
4572
+ force: external_exports.boolean().optional().describe("\uC800\uC7A5 \uC804 \uC790\uB3D9 \uC810\uAC80(\uADFC\uAC70 \uC5C6\uB294 \uC218\uCE58)\uC5D0\uC11C \uB9C9\uD614\uC744 \uB54C, \uC758\uB3C4\uD55C \uAC12\uC784\uC744 \uD655\uC778\uD558\uACE0 \uADF8\uB300\uB85C \uC800\uC7A5"),
4573
+ strict: external_exports.boolean().optional().describe("\uC774\uBC88 \uC800\uC7A5\uB9CC \uC5C4\uACA9\uD558\uAC8C \uC810\uAC80(\uC774\uB825\uC11C \uBCF8\uBB38\uC5D0 \uADFC\uAC70 \uC5C6\uB294 \uC218\uCE58\uB3C4 \uCC28\uB2E8). \uC0DD\uB7B5\uD558\uBA74 \uC800\uC7A5\uB41C \uBAA8\uB4DC \uC124\uC815\uC744 \uB530\uB984")
4572
4574
  });
4573
4575
  var CoverLetterVersionRecordSchema = external_exports.object({
4574
4576
  id,
@@ -28,6 +28,7 @@
28
28
  <div class="sidebar__foot">
29
29
  <div><span class="dot"></span><span id="foot-status">로컬에서 실행 중</span></div>
30
30
  <div id="foot-path" class="truncate" style="margin-top:4px"></div>
31
+ <a class="sidebar__foot-link" href="https://github.com/osntak/careermate" target="_blank" rel="noopener" title="GitHub에서 소스 보기"><svg viewBox="0 0 16 16" width="13" height="13" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg><span>GitHub</span></a>
31
32
  </div>
32
33
  </aside>
33
34
 
@@ -19,6 +19,7 @@ export async function render(ctx) {
19
19
  DataLocation(info),
20
20
  Connection(info),
21
21
  Theme(),
22
+ VerifyMode(info),
22
23
  MyData(info.counts),
23
24
  Backup(backups, () => render(ctx)),
24
25
  DangerZone(ctx),
@@ -135,6 +136,50 @@ function Theme() {
135
136
  });
136
137
  }
137
138
 
139
+ /* ------------------------------------------------- 자소서 수치 점검 모드 */
140
+ function VerifyMode(info) {
141
+ const options = [
142
+ { value: false, label: '기본', desc: '저장된 경력·이력서·프로젝트에 아예 근거가 없는 수치만 차단' },
143
+ { value: true, label: '엄격', desc: '이력서 본문에 없고 구조화 경력/프로젝트 항목에만 있는 수치까지 차단' },
144
+ ];
145
+ let current = !!info.verify_strict;
146
+ const row = el('div', { class: 'flex gap-2 wrap' });
147
+
148
+ async function set(value) {
149
+ if (value === current) return;
150
+ try {
151
+ const res = await post('/api/settings/verify-mode', { strict: value });
152
+ current = !!res.verify_strict;
153
+ toastOk(current ? '엄격 모드를 켰어요.' : '엄격 모드를 껐어요.');
154
+ paint();
155
+ } catch (e) {
156
+ toastError(e);
157
+ }
158
+ }
159
+
160
+ function paint() {
161
+ mount(row, ...options.map((o) => {
162
+ const b = Btn(o.label, { variant: 'ghost', onClick: () => set(o.value) });
163
+ b.setAttribute('aria-pressed', String(o.value === current));
164
+ if (o.value === current) b.classList.add('is-selected');
165
+ return b;
166
+ }));
167
+ }
168
+ paint();
169
+
170
+ const note = el('div', { class: 'muted text-sm', style: { lineHeight: '1.7', marginTop: '10px' } },
171
+ el('div', {}, el('span', { class: 'strong' }, '기본'), ' — ', options[0].desc),
172
+ el('div', {}, el('span', { class: 'strong' }, '엄격'), ' — ', options[1].desc),
173
+ el('div', { style: { marginTop: '6px' } },
174
+ '엄격 모드에선 올린 이력서에 없는 수치가 막힐 수 있어요. 막히면 이력서를 올리거나 모드를 끄세요. AI에게 "엄격하게 봐줘"라고 해도 켤 수 있습니다.'));
175
+
176
+ return Card({
177
+ title: '자소서 수치 점검',
178
+ sub: 'AI가 쓴 수치가 내 실제 데이터에 근거가 있는지 저장 전에 자동으로 확인합니다',
179
+ body: el('div', { class: 'stack-2' }, row, note),
180
+ });
181
+ }
182
+
138
183
  /* ---------------------------------------------------------- 내 데이터 */
139
184
  function MyData(counts = {}) {
140
185
  const items = Object.entries(COUNT_LABELS).map(([key, label]) =>
@@ -145,6 +145,8 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
145
145
  .nav__item.is-active .nav__badge { background: var(--accent); color: var(--text-on-accent); }
146
146
  .sidebar__foot { padding: 12px 16px; border-top: 1px solid var(--border); font-size: 12px; color: var(--text-tertiary); }
147
147
  .sidebar__foot .dot { display:inline-block; width:7px; height:7px; border-radius:999px; background: var(--green); margin-right:6px; }
148
+ .sidebar__foot-link { display:inline-flex; align-items:center; gap:6px; margin-top:8px; color: var(--text-tertiary); text-decoration:none; }
149
+ .sidebar__foot-link:hover { color: var(--text-secondary); }
148
150
 
149
151
  .main { display: flex; flex-direction: column; min-width: 0; }
150
152
  .topbar {