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.
- package/INSTALL.md +12 -8
- package/README.md +14 -8
- package/dist/career-os/knowledge/interview-behavioral.md +9 -1
- package/dist/career-os/knowledge/offer-evaluation-decision.md +2 -2
- package/dist/career-os/knowledge/onboarding-first-90-days.md +1 -1
- package/dist/career-os/knowledge/verifiers/human-voice.md +20 -0
- package/dist/doctor.mjs +421 -11
- package/dist/mcp.mjs +421 -11
- package/dist/migrate.mjs +3 -1
- package/dist/public/index.html +1 -0
- package/dist/public/pages/settings.js +45 -0
- package/dist/public/styles.css +2 -0
- package/dist/site/index.html +19 -12
- package/dist/site/llms-install.txt +11 -8
- package/dist/site/start.html +16 -9
- package/dist/site/style.css +9 -0
- package/dist/web.mjs +351 -6
- package/docs/SUPPORTED_AI_APPS.md +6 -6
- package/docs/career-os/knowledge/interview-behavioral.md +9 -1
- package/docs/career-os/knowledge/offer-evaluation-decision.md +2 -2
- package/docs/career-os/knowledge/onboarding-first-90-days.md +1 -1
- package/docs/career-os/knowledge/verifiers/human-voice.md +20 -0
- package/package.json +1 -1
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/services.ts +61 -2
- package/packages/core/src/verify/index.ts +11 -0
- package/packages/core/src/verify/lexicons.ts +149 -0
- package/packages/core/src/verify/lint.ts +248 -0
- package/packages/core/src/verify/normalize.ts +112 -0
- package/packages/db/src/index.ts +1 -0
- package/packages/db/src/settings.ts +41 -0
- package/packages/mcp-tools/src/tools.ts +80 -6
- package/packages/shared/src/schemas.ts +8 -0
package/dist/web.mjs
CHANGED
|
@@ -39746,6 +39746,26 @@ function fromJson(text, fallback) {
|
|
|
39746
39746
|
var toBit = (b) => b ? 1 : 0;
|
|
39747
39747
|
var fromBit = (n) => Number(n) === 1;
|
|
39748
39748
|
|
|
39749
|
+
// packages/db/src/settings.ts
|
|
39750
|
+
var SETTING_PREFIX = "setting.";
|
|
39751
|
+
function getSetting(key) {
|
|
39752
|
+
const row = getDb().prepare(`SELECT value FROM _meta WHERE key = ?`).get(SETTING_PREFIX + key);
|
|
39753
|
+
return row ? row.value : null;
|
|
39754
|
+
}
|
|
39755
|
+
function setSetting(key, value) {
|
|
39756
|
+
getDb().prepare(
|
|
39757
|
+
`INSERT INTO _meta (key, value) VALUES (?, ?)
|
|
39758
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
39759
|
+
).run(SETTING_PREFIX + key, value);
|
|
39760
|
+
}
|
|
39761
|
+
function getVerifyStrict() {
|
|
39762
|
+
return getSetting("verify_strict") === "1";
|
|
39763
|
+
}
|
|
39764
|
+
function setVerifyStrict(on) {
|
|
39765
|
+
setSetting("verify_strict", on ? "1" : "0");
|
|
39766
|
+
return on;
|
|
39767
|
+
}
|
|
39768
|
+
|
|
39749
39769
|
// packages/shared/src/enums.ts
|
|
39750
39770
|
var APPLICATION_STATUSES = [
|
|
39751
39771
|
"draft",
|
|
@@ -44038,7 +44058,9 @@ var CoverLetterVersionInputSchema = external_exports.object({
|
|
|
44038
44058
|
content: reqBody,
|
|
44039
44059
|
note: optNote.describe("\uC774 \uBC84\uC804\uC758 \uBCC0\uACBD \uC694\uC57D (\uC608: \uC9C0\uC6D0\uB3D9\uAE30 \uBCF4\uAC15)"),
|
|
44040
44060
|
source: external_exports.enum(CONTENT_SOURCES).optional(),
|
|
44041
|
-
set_current: external_exports.boolean().optional().describe("\uC774 \uBC84\uC804\uC744 \uD604\uC7AC \uBC84\uC804\uC73C\uB85C \uC9C0\uC815 (\uAE30\uBCF8 true)")
|
|
44061
|
+
set_current: external_exports.boolean().optional().describe("\uC774 \uBC84\uC804\uC744 \uD604\uC7AC \uBC84\uC804\uC73C\uB85C \uC9C0\uC815 (\uAE30\uBCF8 true)"),
|
|
44062
|
+
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"),
|
|
44063
|
+
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")
|
|
44042
44064
|
});
|
|
44043
44065
|
var CoverLetterVersionRecordSchema = external_exports.object({
|
|
44044
44066
|
id,
|
|
@@ -44200,7 +44222,7 @@ var ApplicationContextSchema = external_exports.object({
|
|
|
44200
44222
|
});
|
|
44201
44223
|
|
|
44202
44224
|
// packages/shared/src/build-info.ts
|
|
44203
|
-
var APP_VERSION = true ? "0.0.
|
|
44225
|
+
var APP_VERSION = true ? "0.0.2" : "0.0.0-dev";
|
|
44204
44226
|
var BUNDLED = true;
|
|
44205
44227
|
|
|
44206
44228
|
// packages/shared/src/index.ts
|
|
@@ -45261,7 +45283,313 @@ function getApplicationContext(opts = {}) {
|
|
|
45261
45283
|
};
|
|
45262
45284
|
}
|
|
45263
45285
|
|
|
45286
|
+
// packages/core/src/verify/normalize.ts
|
|
45287
|
+
var ZERO_WIDTH = /[-]/g;
|
|
45288
|
+
function normalizeText(s) {
|
|
45289
|
+
return (s ?? "").normalize("NFKC").replace(ZERO_WIDTH, "");
|
|
45290
|
+
}
|
|
45291
|
+
var MAGNITUDE = { "\uC5B5": 1e8, "\uB9CC": 1e4, "\uCC9C": 1e3 };
|
|
45292
|
+
var RISK_UNITS = /* @__PURE__ */ new Set([
|
|
45293
|
+
"%",
|
|
45294
|
+
"\uBC30",
|
|
45295
|
+
"\uC6D0",
|
|
45296
|
+
"\uAC74",
|
|
45297
|
+
"\uBA85",
|
|
45298
|
+
"\uC810",
|
|
45299
|
+
"\uC704",
|
|
45300
|
+
"\uB144\uCC28",
|
|
45301
|
+
"\uB144",
|
|
45302
|
+
"\uAC1C\uC6D4",
|
|
45303
|
+
"\uC8FC",
|
|
45304
|
+
"\uC2DC\uAC04",
|
|
45305
|
+
"\uD68C",
|
|
45306
|
+
"\uAC1C"
|
|
45307
|
+
]);
|
|
45308
|
+
var NUM_RE = /(\d{1,3}(?:,\d{3})+|\d+)(?:\.(\d+))?\s*(억|만|천)?\s*(원|%p|%|%|퍼센트|배|건|명|점|위|년차|개월|년|시간|주|회|개)?/g;
|
|
45309
|
+
var YEAR_RE = /^(?:19|20)\d{2}$/;
|
|
45310
|
+
function normUnit(u) {
|
|
45311
|
+
if (u === "\uFF05" || u === "\uD37C\uC13C\uD2B8" || u === "%p") return "%";
|
|
45312
|
+
return u;
|
|
45313
|
+
}
|
|
45314
|
+
function extractNumbers(text) {
|
|
45315
|
+
const t = normalizeText(text);
|
|
45316
|
+
const out = [];
|
|
45317
|
+
for (const m of t.matchAll(NUM_RE)) {
|
|
45318
|
+
const intPart = m[1].replace(/,/g, "");
|
|
45319
|
+
const frac = m[2] ? "." + m[2] : "";
|
|
45320
|
+
const base = Number(intPart + frac);
|
|
45321
|
+
if (!Number.isFinite(base)) continue;
|
|
45322
|
+
const mag = m[3] ?? "";
|
|
45323
|
+
let unit = normUnit(m[4] ?? "");
|
|
45324
|
+
if (!mag && (!unit || unit === "\uB144") && YEAR_RE.test(intPart) && !frac) continue;
|
|
45325
|
+
const scale = MAGNITUDE[mag] ?? 1;
|
|
45326
|
+
const value = base * scale;
|
|
45327
|
+
if (mag && !unit) unit = "\uC6D0";
|
|
45328
|
+
const surface = m[0].trim();
|
|
45329
|
+
if (!surface) continue;
|
|
45330
|
+
const risk = RISK_UNITS.has(unit) || !unit && value >= 1e3 || !!mag && !unit;
|
|
45331
|
+
out.push({ raw: surface, value, unit, risk });
|
|
45332
|
+
}
|
|
45333
|
+
return out;
|
|
45334
|
+
}
|
|
45335
|
+
function splitSegments(text) {
|
|
45336
|
+
return normalizeText(text).split(/[\n.。!?!?·••]|(?:\s-\s)/).map((s) => s.trim()).filter(Boolean);
|
|
45337
|
+
}
|
|
45338
|
+
function addToMap(map2, unit, value) {
|
|
45339
|
+
const arr = map2.get(unit);
|
|
45340
|
+
if (arr) arr.push(value);
|
|
45341
|
+
else map2.set(unit, [value]);
|
|
45342
|
+
}
|
|
45343
|
+
function buildCorpus(text) {
|
|
45344
|
+
const byUnit = /* @__PURE__ */ new Map();
|
|
45345
|
+
const segments = [];
|
|
45346
|
+
for (const seg of splitSegments(text)) {
|
|
45347
|
+
const segMap = /* @__PURE__ */ new Map();
|
|
45348
|
+
for (const n of extractNumbers(seg)) {
|
|
45349
|
+
addToMap(byUnit, n.unit, n.value);
|
|
45350
|
+
addToMap(segMap, n.unit, n.value);
|
|
45351
|
+
}
|
|
45352
|
+
if (segMap.size) segments.push(segMap);
|
|
45353
|
+
}
|
|
45354
|
+
return { byUnit, segments };
|
|
45355
|
+
}
|
|
45356
|
+
|
|
45357
|
+
// packages/core/src/verify/lexicons.ts
|
|
45358
|
+
var G = (src, flags = "g") => new RegExp(src, flags);
|
|
45359
|
+
var GENERIC_OPENERS = {
|
|
45360
|
+
id: "generic_opener",
|
|
45361
|
+
label: "\uC81C\uB108\uB9AD\xB7\uC0C1\uD22C\uC801 \uB3C4\uC785(\uD68C\uC0AC\uBA85 \uBC14\uAFD4\uB3C4 \uB9D0\uC774 \uB418\uB294 \uC2DC\uC791)",
|
|
45362
|
+
regex: G(
|
|
45363
|
+
[
|
|
45364
|
+
"i am writing to apply",
|
|
45365
|
+
"i have always been passionate",
|
|
45366
|
+
"as a recent graduate",
|
|
45367
|
+
"to whom it may concern",
|
|
45368
|
+
"\uC9C0\uC6D0\uD558\uAC8C \uB418\uC5C8\uC2B5\uB2C8\uB2E4",
|
|
45369
|
+
"\uC9C0\uC6D0\uD569\uB2C8\uB2E4",
|
|
45370
|
+
"\uC5B4\uB9B4 ?\uC801\uBD80\uD130",
|
|
45371
|
+
"\uBCF4\uBA70 \uC790\uB790\uC2B5\uB2C8\uB2E4",
|
|
45372
|
+
"\uC800\uB294 .{0,10}\uD55C \uC0AC\uB78C\uC785\uB2C8\uB2E4",
|
|
45373
|
+
"\uD3C9\uC18C .{0,14}\uAD00\uC2EC\uC774 \uB9CE\uC544",
|
|
45374
|
+
"\uD56D\uC0C1 .{0,12}\uBBFF\uC5B4",
|
|
45375
|
+
"\uB370\uC774\uD130\uC758 \uD798\uC744 \uBBFF"
|
|
45376
|
+
].join("|"),
|
|
45377
|
+
"gi"
|
|
45378
|
+
)
|
|
45379
|
+
};
|
|
45380
|
+
var EMPTY_ADJECTIVES = {
|
|
45381
|
+
id: "empty_adjective",
|
|
45382
|
+
label: "\uC99D\uAC70 \uC5C6\uB294 \uD3C9\uAC00\uD615 \uD615\uC6A9\uC0AC\xB7\uC790\uAE30\uC11C\uC220",
|
|
45383
|
+
regex: G(
|
|
45384
|
+
[
|
|
45385
|
+
"\uC131\uC2E4(\uD558|\uD55C|\uD568)",
|
|
45386
|
+
"\uCC45\uC784\uAC10\uC774 \uAC15",
|
|
45387
|
+
"\uC5F4\uC815(\uC744 \uAC00\uC9C0|\uC801)",
|
|
45388
|
+
"\uC18C\uD1B5\uC744 \uC798",
|
|
45389
|
+
"\uCEE4\uBBA4\uB2C8\uCF00\uC774\uC158\uC774 \uC6D0\uD65C",
|
|
45390
|
+
"\uAF3C\uAF3C(\uD558|\uD55C|\uD568)",
|
|
45391
|
+
"\uB048\uAE30(\uC788|\uAC00)",
|
|
45392
|
+
"\uD5CC\uC2E0\uC801",
|
|
45393
|
+
"\uC8FC\uB3C4\uC801\uC778",
|
|
45394
|
+
"\uC131\uC7A5\uD558\uB294 \uC778\uC7AC",
|
|
45395
|
+
"\uB77C\uACE0 \uC0DD\uAC01\uD569\uB2C8\uB2E4",
|
|
45396
|
+
"\uB9CE\uC740 \uAC83\uC744 \uBC30\uC6E0",
|
|
45397
|
+
"\uD1B5\uD574 \uC131\uC7A5",
|
|
45398
|
+
"\uAE30\uC5EC\uD558\uACE0 \uC2F6",
|
|
45399
|
+
"\uCD5C\uC120\uC744 \uB2E4",
|
|
45400
|
+
"\uBCF4\uD0EC\uC774 \uB418\uACA0",
|
|
45401
|
+
"team player",
|
|
45402
|
+
"hard-?working",
|
|
45403
|
+
"detail-?oriented",
|
|
45404
|
+
"motivated",
|
|
45405
|
+
"proactive",
|
|
45406
|
+
"dedicated",
|
|
45407
|
+
"diligent"
|
|
45408
|
+
].join("|"),
|
|
45409
|
+
"gi"
|
|
45410
|
+
)
|
|
45411
|
+
};
|
|
45412
|
+
var CLICHES = {
|
|
45413
|
+
id: "cliche",
|
|
45414
|
+
label: "AI \uD074\uB9AC\uC170\xB7\uACF5\uD5C8\uD55C \uC0C1\uD22C\uC5B4",
|
|
45415
|
+
regex: G(
|
|
45416
|
+
[
|
|
45417
|
+
"\uAE09\uBCC0\uD558\uB294 \uC2DC\uB300",
|
|
45418
|
+
"4\uCC28 \uC0B0\uC5C5\uD601\uBA85",
|
|
45419
|
+
"\uB04A\uC784\uC5C6\uB294 \uB178\uB825",
|
|
45420
|
+
"\uADC0\uC0AC\uC758 \uBC1C\uC804\uC5D0 \uAE30\uC5EC",
|
|
45421
|
+
"\uADC0\uC0AC",
|
|
45422
|
+
"\uC18C\uD1B5\uACFC \uD611\uC5C5",
|
|
45423
|
+
"\uAE00\uB85C\uBC8C \uC778\uC7AC",
|
|
45424
|
+
"\uBB34\uD55C\uD55C \uAC00\uB2A5\uC131",
|
|
45425
|
+
"\uD55C \uB2E8\uACC4 \uB354 \uC131\uC7A5"
|
|
45426
|
+
].join("|")
|
|
45427
|
+
)
|
|
45428
|
+
};
|
|
45429
|
+
var TRANSLATIONESE = {
|
|
45430
|
+
id: "translationese",
|
|
45431
|
+
label: "\uBC88\uC5ED\uD22C \uBB38\uBC95(\uC601\uC5B4 \uC9C1\uC5ED\uCCB4)",
|
|
45432
|
+
regex: G(
|
|
45433
|
+
[
|
|
45434
|
+
"\uC5D0 \uB300\uD55C",
|
|
45435
|
+
"\uC744 \uD1B5\uD574",
|
|
45436
|
+
"\uB97C \uD1B5\uD574",
|
|
45437
|
+
"\uC5D0 \uC788\uC5B4\uC11C",
|
|
45438
|
+
"\uB77C\uB294 \uC810\uC5D0\uC11C",
|
|
45439
|
+
"\uAC00\uC9C0\uACE0 \uC788\uC2B5\uB2C8\uB2E4",
|
|
45440
|
+
"\uB418\uC5B4\uC84C",
|
|
45441
|
+
"\uB418\uC5B4\uC9D1\uB2C8\uB2E4",
|
|
45442
|
+
"\uD568\uC73C\uB85C\uC368",
|
|
45443
|
+
"\uC5D0 \uB2E4\uB984 \uC544\uB2C8",
|
|
45444
|
+
"\uC9C4\uD589\uD558\uC600\uC2B5\uB2C8\uB2E4"
|
|
45445
|
+
].join("|")
|
|
45446
|
+
)
|
|
45447
|
+
};
|
|
45448
|
+
var HYPE = {
|
|
45449
|
+
id: "hype",
|
|
45450
|
+
label: "\uACFC\uC7A5 \uC218\uC2DD\uC5B4",
|
|
45451
|
+
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("|"))
|
|
45452
|
+
};
|
|
45453
|
+
var BLIND_PII = {
|
|
45454
|
+
id: "blind_pii",
|
|
45455
|
+
label: "\uBE14\uB77C\uC778\uB4DC \uCC44\uC6A9 \uC704\uBC18 \uC18C\uC9C0(\uC9C1\uBB34\uBB34\uAD00 \uAC1C\uC778\uC815\uBCF4)",
|
|
45456
|
+
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("|"))
|
|
45457
|
+
};
|
|
45458
|
+
var STYLE_LEXICONS = [
|
|
45459
|
+
GENERIC_OPENERS,
|
|
45460
|
+
EMPTY_ADJECTIVES,
|
|
45461
|
+
CLICHES,
|
|
45462
|
+
TRANSLATIONESE,
|
|
45463
|
+
HYPE,
|
|
45464
|
+
BLIND_PII
|
|
45465
|
+
];
|
|
45466
|
+
function countHits(lex, text, maxHits = 8) {
|
|
45467
|
+
const hits = [];
|
|
45468
|
+
let count = 0;
|
|
45469
|
+
for (const m of text.matchAll(lex.regex)) {
|
|
45470
|
+
count++;
|
|
45471
|
+
if (hits.length < maxHits) hits.push(m[0].trim());
|
|
45472
|
+
}
|
|
45473
|
+
return { count, hits };
|
|
45474
|
+
}
|
|
45475
|
+
|
|
45476
|
+
// packages/core/src/verify/lint.ts
|
|
45477
|
+
var within = (a, b) => Math.abs(a - b) <= Math.max(0.5, 0.02 * Math.max(Math.abs(a), Math.abs(b)));
|
|
45478
|
+
var approx = (a, b) => Math.abs(a - b) <= Math.max(0.5, 0.01 * Math.max(Math.abs(a), Math.abs(b)));
|
|
45479
|
+
function isExplained(token, corpus) {
|
|
45480
|
+
const v = token.value;
|
|
45481
|
+
for (const s of corpus.byUnit.get(token.unit) ?? []) if (within(v, s)) return true;
|
|
45482
|
+
for (const seg of corpus.segments) {
|
|
45483
|
+
const vals = seg.get(token.unit);
|
|
45484
|
+
if (vals && vals.length >= 2) {
|
|
45485
|
+
for (let i = 0; i < vals.length; i++)
|
|
45486
|
+
for (let j = 0; j < vals.length; j++) {
|
|
45487
|
+
if (i === j) continue;
|
|
45488
|
+
if (approx(v, vals[i] - vals[j]) || approx(v, vals[i] + vals[j])) return true;
|
|
45489
|
+
}
|
|
45490
|
+
}
|
|
45491
|
+
}
|
|
45492
|
+
if (token.unit === "%") {
|
|
45493
|
+
for (const seg of corpus.segments) {
|
|
45494
|
+
for (const [u, vals] of seg) {
|
|
45495
|
+
if (u === "%" || vals.length < 2) continue;
|
|
45496
|
+
for (let i = 0; i < vals.length; i++)
|
|
45497
|
+
for (let j = 0; j < vals.length; j++) {
|
|
45498
|
+
if (i === j) continue;
|
|
45499
|
+
const a = vals[i];
|
|
45500
|
+
const b = vals[j];
|
|
45501
|
+
if (b !== 0 && (approx(v, a / b * 100) || approx(v, (a - b) / b * 100) || approx(v, (a - b) / a * 100)))
|
|
45502
|
+
return true;
|
|
45503
|
+
if (a + b !== 0 && approx(v, a / (a + b) * 100)) return true;
|
|
45504
|
+
}
|
|
45505
|
+
}
|
|
45506
|
+
}
|
|
45507
|
+
}
|
|
45508
|
+
return false;
|
|
45509
|
+
}
|
|
45510
|
+
function analyzeProvenance(outputText, corpus) {
|
|
45511
|
+
const documents = buildCorpus(corpus.documents);
|
|
45512
|
+
const structured = buildCorpus(corpus.structured);
|
|
45513
|
+
const job = buildCorpus(corpus.job);
|
|
45514
|
+
const nums = extractNumbers(outputText).filter((n) => n.risk);
|
|
45515
|
+
const res = { checked: nums.length, supported: 0, fabricated: [], unverified: [], jobSourced: [] };
|
|
45516
|
+
for (const n of nums) {
|
|
45517
|
+
const tok = { raw: n.raw, value: n.value, unit: n.unit };
|
|
45518
|
+
if (isExplained(n, documents)) res.supported++;
|
|
45519
|
+
else if (isExplained(n, structured)) res.unverified.push({ ...tok, reason: "structured_only" });
|
|
45520
|
+
else if (isExplained(n, job)) res.jobSourced.push({ ...tok, reason: "job_only" });
|
|
45521
|
+
else res.fabricated.push({ ...tok, reason: "fabricated" });
|
|
45522
|
+
}
|
|
45523
|
+
return res;
|
|
45524
|
+
}
|
|
45525
|
+
function styleSignals(text) {
|
|
45526
|
+
const t = normalizeText(text);
|
|
45527
|
+
const signals = [];
|
|
45528
|
+
for (const lex of STYLE_LEXICONS) {
|
|
45529
|
+
const { count, hits } = countHits(lex, t);
|
|
45530
|
+
if (count > 0) {
|
|
45531
|
+
const severity = lex.id === "blind_pii" || lex.id === "generic_opener" ? "warn" : "info";
|
|
45532
|
+
signals.push({ id: lex.id, label: lex.label, severity, count, hits });
|
|
45533
|
+
}
|
|
45534
|
+
}
|
|
45535
|
+
return signals;
|
|
45536
|
+
}
|
|
45537
|
+
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.";
|
|
45538
|
+
function lintArtifact(kind, text, corpus, opts = {}) {
|
|
45539
|
+
const norm = normalizeText(text);
|
|
45540
|
+
const corpusAvailable = (corpus.documents + corpus.structured).trim().length > 0;
|
|
45541
|
+
const provenance = analyzeProvenance(text, corpus);
|
|
45542
|
+
const signals = styleSignals(text);
|
|
45543
|
+
const strict = !!opts.strict;
|
|
45544
|
+
const blocking = [];
|
|
45545
|
+
if (corpusAvailable && kind === "cover_letter") {
|
|
45546
|
+
if (provenance.fabricated.length > 0) {
|
|
45547
|
+
blocking.push({
|
|
45548
|
+
id: "fabricated_numbers",
|
|
45549
|
+
label: "\uADFC\uAC70 \uC5C6\uB294 \uC218\uCE58(\uC800\uC7A5\uB41C \uACBD\uB825\xB7\uC774\uB825\uC11C\uC5D0 \uC5C6\uC74C)",
|
|
45550
|
+
detail: provenance.fabricated.map((f) => f.raw).join(", ")
|
|
45551
|
+
});
|
|
45552
|
+
}
|
|
45553
|
+
if (strict && provenance.unverified.length > 0) {
|
|
45554
|
+
blocking.push({
|
|
45555
|
+
id: "unverified_numbers_strict",
|
|
45556
|
+
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)",
|
|
45557
|
+
detail: provenance.unverified.map((u) => u.raw).join(", ")
|
|
45558
|
+
});
|
|
45559
|
+
}
|
|
45560
|
+
}
|
|
45561
|
+
return {
|
|
45562
|
+
kind,
|
|
45563
|
+
charCount: [...norm].length,
|
|
45564
|
+
signals,
|
|
45565
|
+
provenance,
|
|
45566
|
+
blocking,
|
|
45567
|
+
corpusAvailable,
|
|
45568
|
+
strict,
|
|
45569
|
+
disclaimer: DISCLAIMER
|
|
45570
|
+
};
|
|
45571
|
+
}
|
|
45572
|
+
|
|
45264
45573
|
// packages/core/src/services.ts
|
|
45574
|
+
function collectVerifyCorpus(jobId) {
|
|
45575
|
+
const documents = [];
|
|
45576
|
+
for (const d of documentRepo.list()) {
|
|
45577
|
+
if (d.source !== "ai") documents.push(d.content);
|
|
45578
|
+
}
|
|
45579
|
+
const structured = [];
|
|
45580
|
+
for (const e of experienceRepo.list()) {
|
|
45581
|
+
structured.push([e.company, e.role, e.description, ...e.achievements ?? [], ...e.tech ?? []].filter(Boolean).join("\n"));
|
|
45582
|
+
}
|
|
45583
|
+
for (const p of projectRepo.list()) {
|
|
45584
|
+
structured.push([p.name, p.description, ...p.highlights ?? [], ...p.tech ?? []].filter(Boolean).join("\n"));
|
|
45585
|
+
}
|
|
45586
|
+
for (const s of skillRepo.list()) {
|
|
45587
|
+
structured.push([s.name, s.level, s.years != null ? `${s.years}\uB144` : ""].filter(Boolean).join(" "));
|
|
45588
|
+
}
|
|
45589
|
+
const job = jobId ? jobRepo.get(jobId) : null;
|
|
45590
|
+
const jobText = job ? [job.description, ...job.requirements ?? [], ...job.keywords ?? []].filter(Boolean).join("\n") : "";
|
|
45591
|
+
return { documents: documents.join("\n"), structured: structured.join("\n"), job: jobText };
|
|
45592
|
+
}
|
|
45265
45593
|
function saveProfile(input) {
|
|
45266
45594
|
const p = profileRepo.save(input);
|
|
45267
45595
|
activityRepo.log("profile_updated", `\uD504\uB85C\uD544\uC744 ${p.name ? `(${p.name}) ` : ""}\uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4.`, "profile", p.id);
|
|
@@ -45303,6 +45631,15 @@ function saveFitAnalysis(input) {
|
|
|
45303
45631
|
return fit;
|
|
45304
45632
|
}
|
|
45305
45633
|
function saveCoverLetterVersion(input) {
|
|
45634
|
+
const strict = input.strict ?? getVerifyStrict();
|
|
45635
|
+
const verification = lintArtifact("cover_letter", input.content, collectVerifyCorpus(input.job_id), { strict });
|
|
45636
|
+
const forced = verification.blocking.length > 0 && input.force === true;
|
|
45637
|
+
if (verification.blocking.length > 0 && !forced) {
|
|
45638
|
+
const detail = verification.blocking.map((b) => `${b.label}: ${b.detail}`).join("; ");
|
|
45639
|
+
const strictHit = verification.blocking.some((b) => b.id === "unverified_numbers_strict");
|
|
45640
|
+
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).`;
|
|
45641
|
+
throw new Error(`\uC800\uC7A5 \uC804 \uC790\uB3D9 \uC810\uAC80\uC5D0\uC11C \uB9C9\uD614\uC2B5\uB2C8\uB2E4 \u2014 ${detail}. ${tail}`);
|
|
45642
|
+
}
|
|
45306
45643
|
const { coverLetter, version } = coverLetterRepo.addVersion({
|
|
45307
45644
|
cover_letter_id: input.cover_letter_id,
|
|
45308
45645
|
title: input.title,
|
|
@@ -45324,7 +45661,7 @@ function saveCoverLetterVersion(input) {
|
|
|
45324
45661
|
"cover_letter",
|
|
45325
45662
|
coverLetter.id
|
|
45326
45663
|
);
|
|
45327
|
-
return { coverLetter, version };
|
|
45664
|
+
return { coverLetter, version, verification, forced };
|
|
45328
45665
|
}
|
|
45329
45666
|
function updateApplicationStatus(jobId, status, note) {
|
|
45330
45667
|
const job = jobRepo.get(jobId);
|
|
@@ -46407,7 +46744,8 @@ function getServerInfo() {
|
|
|
46407
46744
|
exports_dir: getExportsDir(),
|
|
46408
46745
|
backups_dir: getBackupsDir(),
|
|
46409
46746
|
node_version: process.version,
|
|
46410
|
-
counts: getEntityCounts()
|
|
46747
|
+
counts: getEntityCounts(),
|
|
46748
|
+
verify_strict: getVerifyStrict()
|
|
46411
46749
|
};
|
|
46412
46750
|
}
|
|
46413
46751
|
|
|
@@ -46589,7 +46927,8 @@ function registerApiRoutes(router2) {
|
|
|
46589
46927
|
job_id: input.job_id,
|
|
46590
46928
|
content: input.content,
|
|
46591
46929
|
note: input.note,
|
|
46592
|
-
source: input.source ?? "manual"
|
|
46930
|
+
source: input.source ?? "manual",
|
|
46931
|
+
force: true
|
|
46593
46932
|
});
|
|
46594
46933
|
if (input.is_primary) coverLetterRepo.setPrimary(coverLetter.id);
|
|
46595
46934
|
activityRepo.log("cover_letter_added", `${coverLetter.title} \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "cover_letter", coverLetter.id);
|
|
@@ -46609,7 +46948,7 @@ function registerApiRoutes(router2) {
|
|
|
46609
46948
|
ctx.req,
|
|
46610
46949
|
CoverLetterVersionInputSchema.omit({ cover_letter_id: true })
|
|
46611
46950
|
);
|
|
46612
|
-
const { coverLetter, version } = saveCoverLetterVersion({ ...input, cover_letter_id: id2(ctx) });
|
|
46951
|
+
const { coverLetter, version } = saveCoverLetterVersion({ ...input, cover_letter_id: id2(ctx), force: true });
|
|
46613
46952
|
return { cover_letter: coverLetter, version };
|
|
46614
46953
|
});
|
|
46615
46954
|
router2.put("/api/cover-letters/:id/current-version", async (ctx) => {
|
|
@@ -46691,6 +47030,12 @@ function registerApiRoutes(router2) {
|
|
|
46691
47030
|
content: JSON.stringify(exportAll(), null, 2)
|
|
46692
47031
|
})
|
|
46693
47032
|
);
|
|
47033
|
+
router2.post("/api/settings/verify-mode", async (ctx) => {
|
|
47034
|
+
const { strict } = await readJsonBody(ctx.req, external_exports.object({ strict: external_exports.boolean() }));
|
|
47035
|
+
const on = setVerifyStrict(strict);
|
|
47036
|
+
activityRepo.log("profile_updated", `\uC790\uC18C\uC11C \uC810\uAC80\uC744 ${on ? "\uC5C4\uACA9" : "\uAE30\uBCF8"} \uBAA8\uB4DC\uB85C \uBCC0\uACBD\uD588\uC2B5\uB2C8\uB2E4.`);
|
|
47037
|
+
return { verify_strict: on };
|
|
47038
|
+
});
|
|
46694
47039
|
router2.post("/api/settings/backup", () => createBackup());
|
|
46695
47040
|
router2.post("/api/settings/reset", async (ctx) => {
|
|
46696
47041
|
const { confirm } = await readJsonBody(ctx.req, external_exports.object({ confirm: external_exports.string() }));
|
|
@@ -12,15 +12,15 @@ CareerMate는 LLM이 없는 **로컬 MCP 도구**입니다. *내 컴퓨터에서
|
|
|
12
12
|
|---|---|---|---|
|
|
13
13
|
| **1. 에이전트가 대신 설치** | 작업 폴더에서 **한 문장** → AI가 `careermate.life/llms-install.txt`를 읽고 npm 설치 + MCP 등록 | 필요(에이전트가 winget/brew 또는 nodejs.org로 도와줌) | Claude Code(데스크톱 **"Code" 탭** / CLI)·Codex CLI·Gemini CLI·Cursor 등 *명령 실행 가능한 에이전트* |
|
|
14
14
|
| **2. npm 명령 직접** | 터미널에 `npx -y careermate init` 한 줄(감지된 앱 자동 등록) | 필요 | Claude Desktop·Codex·Cursor 등(그 외는 `--print`로 수동) |
|
|
15
|
-
| **3.
|
|
15
|
+
| **3. `careermate.zip`** | ZIP 압축 해제 후 폴더를 확장 설정에서 추가 (`.mcpb` 직접 추가는 known issue) | **불필요**(Claude Desktop 내장) | **Claude Desktop "채팅" 앱 전용** |
|
|
16
16
|
|
|
17
|
-
**1·2는 같은 메커니즘**(Node + npm/npx + MCP 등록) — 에이전트가 치느냐 / 내가 치느냐의 차이일 뿐입니다. 그래서 1·2의 "대상" 제품군이 서로 **겹칩니다**(같은 Claude Code·Codex·Cursor 등). 어느 앱이 어느 방식을 쓰는지는 아래 **앱 × 방식** 매트릭스로 보세요. **3(
|
|
17
|
+
**1·2는 같은 메커니즘**(Node + npm/npx + MCP 등록) — 에이전트가 치느냐 / 내가 치느냐의 차이일 뿐입니다. 그래서 1·2의 "대상" 제품군이 서로 **겹칩니다**(같은 Claude Code·Codex·Cursor 등). 어느 앱이 어느 방식을 쓰는지는 아래 **앱 × 방식** 매트릭스로 보세요. **3(`careermate.zip`)만 Claude Desktop "채팅" 앱 전용**이라 성격이 다릅니다(내장 Node 사용, 단 버전<22.5면 불안정).
|
|
18
18
|
|
|
19
19
|
### 앱 × 방식
|
|
20
20
|
|
|
21
|
-
| 앱 | 1 에이전트 | 2 npm직접 | 3
|
|
21
|
+
| 앱 | 1 에이전트 | 2 npm직접 | 3 zip | 영구 지침 | MCP 등록 위치 |
|
|
22
22
|
|---|---|---|---|---|---|
|
|
23
|
-
| **Claude Desktop "채팅"** | — | ✅(config 등록) | ✅ | — |
|
|
23
|
+
| **Claude Desktop "채팅"** | — | ✅(config 등록) | ✅ | — | 압축 해제 폴더 또는 `claude_desktop_config.json` |
|
|
24
24
|
| **Claude Code (Code 탭 / CLI)** | ✅ | ✅ | — | 루트 `CLAUDE.md` | 프로젝트 `.mcp.json` |
|
|
25
25
|
| **Codex CLI** (⚠️ 데스크톱 앱 ✗) | ✅ | ✅ | — | 루트 `AGENTS.md` | `~/.codex/config.toml` |
|
|
26
26
|
| **Gemini CLI / Antigravity** | ✅ | ✅ | — | — | Gemini CLI `~/.gemini/settings.json` · Antigravity `~/.gemini/.../mcp_config.json` |
|
|
@@ -37,10 +37,10 @@ CareerMate는 LLM이 없는 **로컬 MCP 도구**입니다. *내 컴퓨터에서
|
|
|
37
37
|
|
|
38
38
|
- **Claude Code**(Code 탭/CLI): **Pro 이상**(무료 플랜 불가) 또는 `ANTHROPIC_API_KEY` 종량제.
|
|
39
39
|
- **Codex**: **ChatGPT Plus 이상** 로그인으로 충분(API 키 불필요).
|
|
40
|
-
-
|
|
40
|
+
- **`careermate.zip`**(Claude Desktop 채팅 확장): **무료 플랜에서도 설치** 가능(안정성이 변수). `.mcpb` 직접 추가는 일부 Claude Desktop 버전에서 실패하는 known issue라 ZIP 폴더 추가를 권장합니다.
|
|
41
41
|
- **터미널/npx**: 무료(Node만). 연결할 앱의 구독 정책은 별개.
|
|
42
42
|
|
|
43
|
-
> **런타임.** CareerMate 서버는 Node 프로그램이라 **Node.js 22.5+ 가 필요**합니다 — 위 1·2 경로 공통(없으면 에이전트가 winget/brew, 안 되면 [nodejs.org](https://nodejs.org) LTS 설치 파일). **Node가 아예 필요 없는 길은
|
|
43
|
+
> **런타임.** CareerMate 서버는 Node 프로그램이라 **Node.js 22.5+ 가 필요**합니다 — 위 1·2 경로 공통(없으면 에이전트가 winget/brew, 안 되면 [nodejs.org](https://nodejs.org) LTS 설치 파일). **Node가 아예 필요 없는 길은 `careermate.zip`뿐**인데 그건 Claude Desktop 내장 Node에 의존해 불안정할 수 있습니다 — **"완전 무설치 + 안정"을 동시에 주는 길은 아직 없습니다.**
|
|
44
44
|
>
|
|
45
45
|
> **Windows + Claude Code(Code 탭)**: [Git for Windows](https://git-scm.com/download/win) 사전 설치가 필요합니다(앱이 없으면 "Git is required" 안내). Claude Code가 **세션 격리(git worktree)와 내부 POSIX 셸 도구**에 Git을 쓰기 때문이며 CareerMate와는 무관합니다. macOS는 Git 기본 포함이라 불필요.
|
|
46
46
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
- **(답변 전, 질문이 겨냥하는 역량/회사가치를 controlled vocabulary로 먼저 식별하고 매핑한다.)** — 구조화 면접의 변별력은 표준화·동일 질문·앵커드 채점표(BARS)에서 나오며, 역량 매핑은 그 일부다(역량 매핑 단독이 우수성의 원인이라는 단정은 과도 단순화). 채용공고 요구 역량에서 역으로 스토리를 고른다. **태그 인플레이션을 막기 위해 역량은 고정 enum을 쓴다(아래 택소노미).** _(출처: U.S. OPM Structured Interviews; Northwestern)_
|
|
17
17
|
- **(스토리 뱅크를 미리 8~12개 구축하고, 각 사례에 probing 답 3종을 함께 준비한다.)** — 한 사례를 여러 질문에 변주해 즉석 누락을 막는다. **Amazon 루프 등에서 후보가 떨어지는 1순위는 첫 답변이 아니라 probing 붕괴이므로, 각 사례에 '왜 그 결정을 했나 / 대안은 무엇이었나 / 되돌아보면 무엇을 바꾸겠나' 답을 미리 채운다.** _(출처: Amazon Behavioral Interview 가이드, UVA·Wisconsin 등)_
|
|
18
18
|
- **(사례는 최신성·직무관련성을 갖춰야 한다.)** — 시니어/경력직에게 8년 전 인턴·학부 프로젝트만 가득한 뱅크는 반려 사유다. 각 사례에 연도·역할단계를 기록하고, 경력직은 최근 N년 내 사례 비율을 확보한다. _(출처: 채용 실무 — recency/relevance, hiring manager review)_
|
|
19
|
-
- **(사실·수치·고유명사는 절대 지어내지 않는다. 실패 사례는 조건부로만 쓴다.)** — 면접관은 probing으로 꾸민 이야기를 검증한다. 실패 사례는 회복·재발방지 행동이 Result보다 길고 구체적일 때만 강점이 되며, 일부 문화권 면접관은 부정 신호로 받을 수 있으므로 '학습·후속 개선'이 핵심이 아닐 거면 쓰지 않는다. _(출처: CareerMate AGENTS.md, Amazon probing follow-ups, UVA)_
|
|
19
|
+
- **(사실·수치·고유명사는 절대 지어내지 않는다. 실패 사례는 조건부로만 쓴다.)** — 면접관은 probing으로 꾸민 이야기를 검증한다. 실패 사례는 회복·재발방지 행동이 Result보다 길고 구체적일 때만 강점이 되며, 일부 문화권 면접관은 부정 신호로 받을 수 있으므로 '학습·후속 개선'이 핵심이 아닐 거면 쓰지 않는다. **커리어 공백·전직 경험을 묻는 질문은 '짧게 사실을 밝히고 현재 준비 상태로 전환'하는 간략한 안심 확인으로 처리하라 — 길게 해명하거나 사과조로 이어가면 오히려 레드플래그 신호로 읽힌다(한국 면접 맥락).** _(출처: CareerMate AGENTS.md, Amazon probing follow-ups, UVA)_
|
|
20
20
|
- **(키워드 스터핑을 경계한다.)** — 회사 가치에 '단어 수준으로 맞추라'는 조언은 과하면 '준비된 앵무새'로 보인다. LP/가치 키워드는 실제 사례의 행동이 그 가치를 **입증**할 때만 쓰고, 키워드를 라벨처럼 나열하지 않는다. 구조화 면접은 adverse impact를 줄이고 법적 방어가능성을 높이는 공정성 도구라는 점도 기억한다. _(출처: U.S. OPM Structured Interviews)_
|
|
21
21
|
|
|
22
22
|
### Do
|
|
@@ -60,6 +60,14 @@
|
|
|
60
60
|
**After:** `[역량: growth-learning]` **S:** 첫 사이드프로젝트에서 사용자 검증 없이 4개월간 기능부터 만들었습니다. **T:** 출시 후 사용자를 모으는 것이 목표였습니다. **A:** 저는 가설 검증을 건너뛴 채 개발에 몰입했고, 출시 후에야 핵심 가정이 틀렸음을 알았습니다. 이후 저는 즉시 10명 인터뷰를 돌려 문제를 재정의하고, 2주짜리 프로토타입으로 다시 검증하는 방식으로 프로세스를 바꿨습니다(재발방지 행동이 Result보다 김). **R:** 원래 제품은 가입 9명에 그쳤지만, 재검증 후 만든 버전은 4주 만에 활성 사용자 120명을 모았습니다(label: WAU, source: 가입 로그). '만들기 전에 검증한다'를 이후 모든 프로젝트의 기본 규칙으로 삼았습니다. **Probing 준비:** (왜) 검증을 건너뛴 이유 / (대안) MVP를 먼저 냈다면 / (되돌아보면) 4개월→2주로 단축한 학습.
|
|
61
61
|
**왜 더 나은가:** Before는 실패를 회피해 자기인식·성장성을 증명하지 못한다. After는 진짜 실패를 정직하게 인정하고, **재발방지 행동을 Result보다 길게** 써 학습을 증명하며(조건부 실패 사례 원칙 충족), 정량 대비(9명→120명)로 회복을 보인다.
|
|
62
62
|
|
|
63
|
+
4. **Before:** 질문: '분석이나 판단이 틀렸던 경험을 말해보세요.' 답변: "저는 항상 데이터를 꼼꼼히 보려고 노력하기 때문에 크게 틀린 경우는 없었던 것 같습니다. 한 번 예상과 다른 결과가 나온 적이 있었는데, 팀원들과 의견을 맞춰 잘 해결했습니다."
|
|
64
|
+
**After:** `[역량: failure-learning, data-driven]` **S:** 작년 분기 프로모션 캠페인 설계 시, 저는 특정 사용자 세그먼트의 구매 전환율을 15% 이상으로 예측하고 예산의 40%를 해당 채널에 배정했습니다. **T:** 캠페인 2주차 중간 데이터를 검토하면서 실제 전환율이 6%에 머물고 있음을 확인했고, 제 초기 가정이 틀렸음을 인정해야 했습니다. **A:** 저는 즉시 원인 분석에 들어가 — 초기 샘플이 실제 세그먼트를 대표하지 못했다는 것(표본 편향)을 파악했습니다. 관련 팀에 중간 결과를 솔직하게 공유하고, 잔여 예산의 70%를 전환율이 검증된 다른 채널로 즉시 재배분하도록 제가 제안해 실행했습니다. 이후 저는 캠페인 예측 프로세스에 '사전 샘플 대표성 검증' 단계를 추가하는 SOP를 작성했습니다. **R:** 재배분 후 2주 전환율이 11%로 회복됐고(label: 후반 2주 구매 전환율, source: 캠페인 대시보드), SOP 도입 이후 다음 분기 두 캠페인에서 초기 예측 오차가 기존 대비 절반 이하로 줄었습니다(label: 예측 오차율, source: 분기 리뷰 보고서). **Probing 준비:** (어떻게 알아챘나) 2주차 중간 점검 지표에서 이탈 감지 — 이 체크포인트가 없었으면 캠페인 전체를 날렸을 것 / (무엇을 바꿨나) SOP에 사전 샘플 대표성 검증 단계 명문화 / (재발 방지) 이후 모든 캠페인에 동일 체크포인트·승인 게이트 적용.
|
|
65
|
+
**왜 더 나은가:** Before는 실수를 회피하고 팀 뒤에 숨어 지적 정직성을 전혀 증명하지 못한다(자기 정당화·남 탓 패턴). After는 ① 틀렸음을 1인칭으로 명시("제 초기 가정이 틀렸음을 인정"), ② 어떻게 발견했는지(중간 점검), ③ 즉각 행동, ④ 재발방지(SOP)를 순서대로 채워 intellectual honesty·error-recovery·calibration 세 축을 모두 커버하며, probing 3종으로 후속 깊이 질문에 견딘다. 실패를 정당화하거나 외부 탓으로 돌리는 순간 지적 성숙도 점수가 0에 수렴한다.
|
|
66
|
+
|
|
67
|
+
5. **Before:** 질문: '요구사항이 불명확하거나 데이터가 지저분한 상황에서 어떻게 시작하나요?' 답변: "저는 일단 분석을 시작해보고 문제가 생기면 그때 확인하는 편입니다. 데이터가 불완전해도 우선 해볼 수 있는 것부터 코딩해보면서 방향을 잡습니다."
|
|
68
|
+
**After:** `[역량: ambiguity, data-driven, collaboration]` **S:** 신사업 팀으로부터 "이탈 위험 사용자를 예측하는 모델을 2주 안에 만들어달라"는 요청을 받았는데, 이탈의 정의·대상 기간·사용 목적이 모두 명시되지 않은 상태였습니다. **T:** 불명확한 요구사항 아래에서 실제로 쓸 수 있는 결과물을 기한 내에 내는 것이 목표였습니다. **A:** 저는 코드를 열기 전에 먼저 세 가지 가정을 서면으로 정리했습니다 — ① 이탈 = 30일 미접속, ② 대상 = 최근 90일 활성 사용자, ③ 출력 = 상위 500명 리스트(CRM 액션용). 이 가정을 담은 1페이지 PRD 초안을 요청 다음 날 이해관계자와 30분 리뷰했고, ②를 '60일 활성'으로, ③을 '확률 스코어 전체'로 수정했습니다. 가정을 확정한 뒤에야 데이터 파이프라인·피처 엔지니어링에 들어갔고, 데이터 품질 문제(중복 이벤트 로그 약 18%)는 필터 로직과 근거를 문서에 명시해 재현 가능하게 처리했습니다. **R:** 1차 모델 Precision@500 = 0.71(label: Precision@500, source: 오프라인 검증셋), 요청팀은 해당 리스트 기반 리인게이지먼트 캠페인을 다음 분기 정식 운영으로 채택했습니다(label: 캠페인 채택 여부, source: 분기 OKR 보고서). **Probing 준비:** (가정이 틀렸으면) 리뷰 단계에서 수정 — 실제로 ②③이 바뀐 선례가 있음 / (데이터 품질 판단 기준) 18% 중복은 제거·문서화했고, 더 큰 편향이 있으면 모델 전에 데이터 파이프라인부터 고쳤을 것 / (우선순위 방법) 가정 검증 → 데이터 탐색 → 피처 → 모델 순서로 고정, 앞 단계 불확실성이 크면 다음 단계를 시작하지 않는다.
|
|
69
|
+
**왜 더 나은가:** Before는 불명확한 상황에서 바로 코드·분석부터 들어가고(가정 불명시), 이해관계자 확인을 후순위로 미루는 전형적 약한 답변이다(ambiguity handling 0점). After는 ① 코드 전에 가정을 명시·서면화, ② 이해관계자와 조기 검증, ③ 데이터 품질 처리를 재현 가능하게 문서화해 stakeholder clarification·prioritization·ambiguity handling 세 역량을 모두 입증하며, probing 3종으로 '가정이 틀렸으면 어떻게 했을 것인가'라는 전형적 후속 질문까지 대비한다.
|
|
70
|
+
|
|
63
71
|
## 3. 검증 루브릭 (연결된 AI가 산출물을 이 기준으로 자가검증)
|
|
64
72
|
|
|
65
73
|
> **전제(중요):** 루브릭은 자유 텍스트가 아니라 **`StoryBankEntry` 구조화 레코드 배열**을 입력으로 받는다. S/T/A/R 경계는 자유 텍스트를 LLM이 의미로 갈라야 하므로, CareerMate는 먼저 `save_interview_prep`으로 STAR를 **필드 분리 저장**하게 한다. 그러면 다수 체크가 필드 non-empty / 패턴 카운트로 결정론적이 된다.
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
- **(BATNA는 현직/무직/타 프로세스로 분기한다)** 매트릭스에 BATNA 열을 반드시 포함하되, 사용자가 **현직자면** "현 직장 유지"를, **무직이면** "다른 진행 중 프로세스" 또는 "무오퍼로 더 기다림"을 BATNA로 둔다. 무직인데 진행 중 프로세스가 없으면 BATNA는 약하므로 그 사실을 명시하고(빈 열로 통과시키지 말 것), 협상·결정 강도를 그에 맞게 낮춘다. _(출처: Harvard PON — BATNA, 1차)_
|
|
16
16
|
- **(복수 오퍼는 만료일을 한 시점으로 정렬한다)** BATNA는 동시에 비교 가능한 살아있는 대안이 있을 때만 협상력으로 작동한다. 빠른 오퍼에는 "진행 중인 다른 최종 단계를 마치기 위해 X일 필요"를 구체 날짜로 요청하고, 느린 곳에는 "이미 오퍼를 받아 일정을 앞당겨달라"고 신호한다. 한 오퍼를 잡아두려면(on_hold) 정중한 관심 재확인 + 구체 사유를 묶는다. _(출처: Harvard PON(BATNA) + MIT Sloan CDO, 1차/2차)_
|
|
17
17
|
- **(total comp은 명목 합이 아니라 세후·다년 현가로 비교한다)** base+stock+bonus 명목 합이 아니라 (1) **세후 실수령**(한국 4대보험·소득세 공제, 비과세 식대·차량유지비 차이), (2) stock의 **연환산·베스팅·cliff**, (3) **다년 임금인상률·승진 속도**(한국 대기업 호봉 자동상승 vs 스타트업 협상제는 4년 누적에서 결과가 뒤집힐 수 있다), (4) **퇴직금·퇴직연금(DC/DB)**이 연봉에 포함인지 별도인지로 비교한다. 명목 단일 숫자는 cliff 후 급감과 세후 격차를 숨긴다. _(출처: levels.fyi(연환산), 벤더 / 한국 세제·퇴직금 분기)_
|
|
18
|
-
- **(stock 분해는 한국 오퍼 유형에 맞춰 적용한다)** "연환산·cliff·refresh" RSU 모델은 미국 빅테크 모델이다. 한국은 **상장 대기업=주식 거의 없음(현금·퇴직금 위주), 비상장 스타트업=스톡옵션(주식매수선택권)·4년 베스팅이 표준이 아닐 수 있음, 중견=현금 위주**다. 비상장 스톡옵션은 행사가·희석·청산우선권으로 기대가치를 할인하고, **행사이익이 근로소득으로 과세되나 벤처기업법 과세특례(연 한도 내 비과세·분할납부)**가 있는지 확인한다. stock이 없는 오퍼에는 stock 행을 비우고 현금·퇴직금·성장으로 비교한다(빈 stock을 가짜로 통과시키지 말 것). _(출처: levels.fyi(연환산) / 벤처기업법 스톡옵션 과세특례, 벤더/한국 1차)_
|
|
18
|
+
- **(stock 분해는 한국 오퍼 유형에 맞춰 적용한다)** "연환산·cliff·refresh" RSU 모델은 미국 빅테크 모델이다. 한국은 **상장 대기업=주식 거의 없음(현금·퇴직금 위주), 비상장 스타트업=스톡옵션(주식매수선택권)·4년 베스팅이 표준이 아닐 수 있음, 중견=현금 위주**다. 비상장 스톡옵션은 행사가·희석·청산우선권으로 기대가치를 할인하고, **행사이익이 근로소득으로 과세되나 벤처기업법 과세특례(연 한도 내 비과세·분할납부)**가 있는지 확인한다. *(과세특례 한도·요건은 현행법 기준이며 개정·사안별로 달라질 수 있음 — 실제 적용 전 최신 세법·세무사 확인 권장)* stock이 없는 오퍼에는 stock 행을 비우고 현금·퇴직금·성장으로 비교한다(빈 stock을 가짜로 통과시키지 말 것). _(출처: levels.fyi(연환산) / 벤처기업법 스톡옵션 과세특례, 벤더/한국 1차)_
|
|
19
19
|
- **(현직 카운터오퍼는 "근본 원인 진단"으로 결정한다 — 기본값 고정 아님)** 카운터오퍼를 받으면 먼저 **"왜 떠나려 했는가"(매니저·성장 정체·번아웃·저평가 연봉)** 를 적고, 그 원인이 돈으로 해결되는지 검증한다. 원인이 **순수 저평가 연봉**이고 카운터가 그것을 시장가로 맞춘다면 수락이 합리적일 수 있다(정답은 "depends"다 — 한쪽으로 못박지 말 것). 원인이 비보상 요인이면 돈은 고치지 못한다. 사용자에게 설명할 때 **"카운터오퍼 수락자 80%가 6개월 내 퇴사" 류 수치는 출처 추적이 거의 불가능한 도시전설(존재 불명 기관 인용)이므로 "통계"가 아니라 "근본 원인 논리"로 설득**한다. _(출처: LHH/Progressive(일화적·출처 검증 불가, 벤더) — 비판적 인용)_
|
|
20
|
-
- **(수락 후 번복은 평판 비용으로, 한국은 채용내정 법리도 본다)** 미국 맥락에서 reneging은 소송보다 **리크루터·추천인·해당 회사 네트워크의 신뢰 상실**이 진짜 비용이다(업계 전체 블랙리스트는 실재하지 않으나 개별 리크루터의 기억은 실재). 다만 **한국은 비대칭**이다: 근로계약서 체결 전 오퍼 단계에서는 지원자 번복의 법적 위험이 낮지만, **회사가 "채용내정"을 일방 취소하면 판례상 근로계약 성립으로 보아 손해배상 다툼 여지**가 있다(at-will 전제의 미국 가이드를 한국에 그대로 적용하지 말 것). 불가피한 번복은 최대한 빨리·사과와 함께·짧은 사유·다리를 태우지 않는 톤으로 통보한다. _(출처: MIT Sloan CDO + CU Boulder(평판 비용, 1차/2차) / 한국 채용내정 법리, 한국 판례)_
|
|
20
|
+
- **(수락 후 번복은 평판 비용으로, 한국은 채용내정 법리도 본다)** 미국 맥락에서 reneging은 소송보다 **리크루터·추천인·해당 회사 네트워크의 신뢰 상실**이 진짜 비용이다(업계 전체 블랙리스트는 실재하지 않으나 개별 리크루터의 기억은 실재). 다만 **한국은 비대칭**이다: 근로계약서 체결 전 오퍼 단계에서는 지원자 번복의 법적 위험이 낮지만, **회사가 "채용내정"을 일방 취소하면 판례상 근로계약 성립으로 보아 손해배상 다툼 여지**가 있다(at-will 전제의 미국 가이드를 한국에 그대로 적용하지 말 것). *(채용내정 성립 여부·손해배상 범위는 사안별 판례 기준이며 개정·상황별로 달라질 수 있음 — 실제 분쟁 시 노무사·변호사 확인 권장)* 불가피한 번복은 최대한 빨리·사과와 함께·짧은 사유·다리를 태우지 않는 톤으로 통보한다. _(출처: MIT Sloan CDO + CU Boulder(평판 비용, 1차/2차) / 한국 채용내정 법리, 한국 판례)_
|
|
21
21
|
- **(입사 전 30-60-90일 정착 계획을 초안한다)** 수락이 정해지면 입사 첫날 전에 0~30일=흡수·학습(기여 아님), 31~60일=초기 성과(early win)·동맹 구축, 61~90일=손익분기 계획을 초안하고, **90일 시점의 "성공 정의"를 매니저와 입사 전/직후 합의**한다. 단 한국에서는 90일 이전에 **전직 통보기간(보통 1개월)·인수인계 의무·경업금지(non-compete) 조항**이 입사일 협상의 제약이므로 함께 점검한다. _(출처: Michael Watkins, The First 90 Days, 1차 / 한국 전직 실무)_
|
|
22
22
|
- **(한국 오퍼레터는 "최종 확정"이 아니라 "처우 조율 단계"로 다루되 구속력은 사안별)** 오퍼레터는 근로계약서와 달리 구속력이 약한 경우가 많아 직급(호봉)·입사일·부서/직무 배치·사이닝 등 **비-연봉 레버**로 협상할 여지가 있다. 단 "구속력 약함"을 단정하지 말고 **"구속력은 사안별이며 채용내정 성립 시 달라진다"** 로 약화한다. **포괄임금제 여부를 확인**해 명목연봉끼리 단순 비교하지 않는다(같은 명목연봉도 실질 시급이 다르다). _(출처: 리멤버 채용 인사이트/잡코리아(2차) + 채용내정 법리(한국 판례))_
|
|
23
23
|
- **(직군별 오퍼 구조 차이를 반영한다)** 백엔드/SRE는 **온콜 수당**, ML·리서치 사이언티스트는 **equity 비중·연구 자율성**, 프론트/PM은 base·성장 경로가 더 무겁고, **신입 vs 시니어 sign-on 관행**도 다르다. 모든 직군에 같은 매트릭스 가중치를 기계적으로 들이대지 말고 직군에 맞춰 축·가중치를 조정한다. _(출처: 직군별 보상 구조 실무 — 보조)_
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
- **(레퍼런스 체크/평판조회를 입사 직전 게이트로 능동 관리한다)** 한국 경력 채용은 입사 직전 평판조회가 빈번하다. (1) reference로 누구를 세울지 사전 선정·동의, (2) 전 직장 연락 타이밍, (3) 부정 평판 리스크(갈등 이력) 사전 맥락화. 이는 salary도 onboarding도 안 다루는 실제 late-stage 게이트다. _(출처: KR 경력 채용 평판조회 관행 — 2차)_
|
|
21
21
|
- **(거절 후 피드백은 1회·짧게·단일질문·비논쟁으로, 단 무응답이 정상임을 전제한다)** 거절 1-3일 후 감사+한 개의 열린 질문("다음에 더 강한 후보가 되려면 한 가지만 꼽아주실 수 있을까요?")+"짧아도 됩니다" 허용+결정 반박 금지. **그러나 콜드 거절 피드백 응답률은 낮다(법무 리스크로 무응답이 디폴트)** — 응답은 보너스로 기대치를 보정하고, **무응답일 때는 인터뷰 로그·역질문 답변 자기복기**를 진짜 재사용 진단으로 삼는다. _(출처: Arcadia/CareerSidekick/4 Corner — 2차/대학 + 무응답 베이스레이트 보정)_
|
|
22
22
|
- **([KR] 처우협의·서면 확정은 연봉 협상 도메인을 따르되, 입사일을 협상 레버로 쓴다)** 연봉/직위/연차/입사일을 **서면 확정한 뒤** 퇴사 통보한다(처우협의 문서팩·포괄임금 정규화는 salary-negotiation 소유). 추가로 이 도메인은 **입사일을 역제안 카드**로 다룬다 — 현 직장 인수인계·연차 소진·경쟁사 전직금지/겸업금지 조항과 충돌을 점검하고, 입사일 유연성을 협상 레버로 활용한다. _(출처: 리멤버 HR/잡코리아 — 2차/KR; 입사일 레버는 본 도메인 고유)_
|
|
23
|
-
- **([KR] 수습=시용 게이트이되, 근로자의 법적 보호를 반드시 병기한다)** 수습/시용은 사용자가 업무능력·조직적합성을 평가하는 게이트이지만, **대법원 판례상 본채용 거부에는 (a)사전에 구체적 평가기준·방법 마련, (b)근로자에 고지, (c)객관적 능력 부족 입증이 필요하고 입증책임은 사용자에게 있다.** 막연한 "근무태도/적응" 인상 평가만으로 거부하면 부당해고가 된다. 따라서 지원자는 **(1) 정규직 전환 기준이 근로계약서에 서면화됐는지 확인·요구할 권리**, **(2) 부당 거부 시 노동위 구제** 권리를 안다. 동시에 첫 90일에 조직문화 적응·OJT 흡수·성실성 신호를 챙기는 것은 실무적으로 유효하다. **수습 디테일: 통상 임금 90% 지급 가능, 다만 수습 3개월 초과 시 최저임금 100% 적용, 5인 미만 사업장은 해고 보호 차등.** _(출처: 대법원 수습 판례/geolabor 노무 — 2차/KR; 권리 측면 병기)_
|
|
23
|
+
- **([KR] 수습=시용 게이트이되, 근로자의 법적 보호를 반드시 병기한다)** 수습/시용은 사용자가 업무능력·조직적합성을 평가하는 게이트이지만, **대법원 판례상 본채용 거부에는 (a)사전에 구체적 평가기준·방법 마련, (b)근로자에 고지, (c)객관적 능력 부족 입증이 필요하고 입증책임은 사용자에게 있다.** 막연한 "근무태도/적응" 인상 평가만으로 거부하면 부당해고가 된다. 따라서 지원자는 **(1) 정규직 전환 기준이 근로계약서에 서면화됐는지 확인·요구할 권리**, **(2) 부당 거부 시 노동위 구제** 권리를 안다. 동시에 첫 90일에 조직문화 적응·OJT 흡수·성실성 신호를 챙기는 것은 실무적으로 유효하다. **수습 디테일: 통상 임금 90% 지급 가능, 다만 수습 3개월 초과 시 최저임금 100% 적용, 5인 미만 사업장은 해고 보호 차등.** *(현행법 기준이며 법령 개정·사안별로 달라질 수 있음 — 실제 적용 전 최신 법령·노무사 확인 권장)* _(출처: 대법원 수습 판례/geolabor 노무 — 2차/KR; 권리 측면 병기)_
|
|
24
24
|
- **([KR] 위계·집단주의 조직에서 "기대치 협상 대화"의 적절성을 분기한다)** Watkins의 다섯 대화는 서구 매니저-부하 1:1 문화 전제다. 한국 위계 조직에서 신입이 매니저에게 "기대치를 협상하자"고 먼저 요청하면 건방지게 비칠 수 있다. **KR에서는 "협상"이 아니라 "우선순위를 여쭙고 정리해 확인받는" 겸손한 프레이밍**으로 치환하고, OJT·사수 관계를 통해 간접적으로 기대치를 합의한다. _(출처: KR 조직문화 분기 — 본 도메인 고유)_
|
|
25
25
|
|
|
26
26
|
### Do
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
> 적용 단위: 한 산출물(자소서 1편, 이력서 1부, 면접 답변 1세트 등). "문장"은 종결부호(`.`, `!`, `?`, `다.`, `요.`) 기준으로 분할한다. 한국어/영어 혼용 글도 동일 규칙을 적용한다.
|
|
19
19
|
|
|
20
|
+
> **수치 임계값 보정 주의 (advisory):** C1(100단어당 1.5개), C3(σ≥5), C6(100단어당 1개) 등의 숫자 기준은 **영어 코퍼스에서 유래한 잠정값**으로, 한국어 산출물에 대해 검증된 적이 없다. 동반 플레이북(`knowledge/human-writing.md`)과 동일한 입장: 이 수치들은 권고 신호(advisory signal)이며 하드 합격/불합격 기준이 아니다. 산출물 유형(이력서 vs 자소서)·길이·업계 문체에 따라 연결 AI가 맥락에 맞게 재해석해야 하고, 향후 한국어 데이터로 재보정되어야 한다.
|
|
21
|
+
|
|
20
22
|
| ID | 검사 | 합격 기준 | 측정 방법(셀 수 있게) |
|
|
21
23
|
|----|------|-----------|------------------------|
|
|
22
24
|
| C1 | 슬롭 단어 밀도 | 슬롭 사전 단어가 **100단어당 1.5개 이하**, 그리고 동일 슬롭 단어가 **3회 초과 반복 없음** | 아래 §슬롭 사전과 대조해 일치 토큰 수를 센다 → (일치 수 ÷ 전체 단어 수) × 100. 단어별 등장 횟수도 집계 |
|
|
@@ -30,6 +32,7 @@
|
|
|
30
32
|
| C9 | 고유명사 보존 | 원본의 회사명·제품명·기술명·자격증명 등 고유명사가 **100% 보존**, 철자 변형 0 | 원본 고유명사 목록 추출 → 대상에서 정확 일치 확인(누락·오타 0) |
|
|
31
33
|
| C10 | 빈말 채움(filler) 문장 | 정보 없는 채움 문장("~에 대한 저의 열정은 남다릅니다" 류)이 **전체 문장의 5% 미만** | §filler 패턴과 매칭되거나 숫자·고유명사·동사 목적어가 전혀 없는 문장 수를 센다 |
|
|
32
34
|
| C11 | 구체 증거(수치·사례) 존재 | 본문에 **정량 근거(숫자/기간/규모/성과 지표)가 최소 2개** 이상 | 숫자+단위/퍼센트/"N배"/"N명"/"N개월" 패턴 매칭 수가 2 이상인지 확인 |
|
|
35
|
+
| C12 | 번역투 문법 밀도 *(advisory)* | 아래 §번역투 패턴이 **100단어당 3개 이하** (권고 신호, 하드 실패 없음) | §번역투 패턴 목록과 부분 매칭 → 밀도 계산. **맥락 확인 필수**: 정상 용법에서도 등장하는 표현이 많으므로 밀도가 높더라도 연결 AI가 문맥을 보고 최종 판단. 단독으로 불합격 처리하지 않는다 |
|
|
33
36
|
|
|
34
37
|
### 슬롭 사전 (C1 — 부분 일치로 카운트)
|
|
35
38
|
|
|
@@ -49,6 +52,23 @@
|
|
|
49
52
|
|
|
50
53
|
문장 안에 (a) 숫자, (b) 고유명사, (c) 구체적 행위 동사+목적어 가운데 **하나도 없으면** 빈말 후보로 카운트. 예: "저는 이 일에 대한 열정이 누구보다 큽니다.", "최선을 다하겠습니다.", "많은 것을 배웠습니다."
|
|
51
54
|
|
|
55
|
+
### 번역투 패턴 사전 (C12 — advisory, 맥락 확인 필수)
|
|
56
|
+
|
|
57
|
+
다음 표현들은 한국어를 영어 구조에서 직역한 번역투의 대표 신호로, LLM 생성 글에서 밀도가 높아지는 경향이 있다. **그러나 자연스러운 한국어 문장에서도 정상적으로 쓰이므로, 단순 존재나 횟수만으로 판정하지 말고 반드시 문맥을 함께 본다.** 밀도(100단어당 3개 초과)가 권고 신호이고, 연결 AI가 최종 판단한다.
|
|
58
|
+
|
|
59
|
+
| 패턴 | 번역투 예시 | 자연스러운 대안 예시 |
|
|
60
|
+
|------|------------|---------------------|
|
|
61
|
+
| `~에 대한` 과다 | "팀 협업에 대한 경험을 보유하고 있습니다" | "팀 협업 경험이 있습니다" |
|
|
62
|
+
| `~을/를 통해` 반복 | "프로젝트를 통해 역량을 통해 성과를 냈습니다" | "프로젝트에서 …을 해냈습니다" |
|
|
63
|
+
| `~에 있어서` | "업무 수행에 있어서 중요한 점은" | "업무를 할 때 중요한 점은" |
|
|
64
|
+
| `~라는 점에서` | "기여했다는 점에서 의미가 있습니다" | "기여한 것이 의미 있습니다" |
|
|
65
|
+
| `가지고 있습니다` | "저는 강한 실행력을 가지고 있습니다" | "저는 실행력이 강합니다" |
|
|
66
|
+
| `되어졌다 / 되어집니다` | "배포가 완료되어졌습니다" | "배포를 완료했습니다" |
|
|
67
|
+
| `~함으로써` | "소통함으로써 문제를 해결함으로써" | "소통해 문제를 해결했습니다" |
|
|
68
|
+
| `~을 진행하였습니다` | "분석을 진행하였습니다" | "분석했습니다" |
|
|
69
|
+
|
|
70
|
+
> **운용 지침:** 위 패턴이 집중적으로 등장하더라도 C12 단독으로는 불합격 처리하지 않는다. C1·C2·C10과 함께 "전체적으로 번역 냄새가 나는가"를 종합 판단하는 보조 신호로만 쓴다. 정상 용법과의 구분이 애매하면 해당 문장을 지적하되 수정 강제는 금지한다.
|
|
71
|
+
|
|
52
72
|
## 3. 불합격 시 처리(루프 연결)
|
|
53
73
|
|
|
54
74
|
연결된 AI는 위 항목을 채점해 **불합격 항목 ID + 근거(센 수치/매칭된 문자열/위치)** 를 모은다. 그다음 항목별로 아래 수정 지시를 적용해 **재작성 → 동일 루브릭 재검증**한다. 재검증 통과까지 반복하되, **C8·C9(사실·고유명사 보존)는 단 1건이라도 위반 시 즉시 하드 실패**로 처리하고 다른 어떤 개선보다 우선해 복구한다.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "careermate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CareerMate — an MCP-first, local-first career workspace. Connect it to a local AI agent (Claude Desktop, Claude Code, Codex); CareerMate stores your career data locally on your machine and exposes it over MCP.",
|
|
6
6
|
"license": "MIT",
|