argus-decision-mcp 1.4.2 → 1.5.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/dist/lib/ambient-elicit.js +7 -5
- package/dist/lib/calendar.js +0 -0
- package/dist/lib/continuity.js +2 -2
- package/dist/lib/detect-signals.js +151 -0
- package/dist/lib/due-note.js +6 -1
- package/dist/lib/elicit.js +9 -1
- package/dist/lib/ledger-append.js +102 -20
- package/dist/lib/ledger-replay.js +58 -18
- package/dist/lib/locale-mismatch.js +2 -2
- package/dist/lib/localize-result.js +215 -18
- package/dist/lib/numeric-drift.js +13 -13
- package/dist/lib/receipt.js +17 -1
- package/dist/lib/render-receipt.js +111 -57
- package/dist/lib/resolve-today.js +34 -0
- package/dist/lib/review/extract-file-node.js +2 -2
- package/dist/lib/review/render.js +6 -6
- package/dist/lib/review/routing.js +6 -6
- package/dist/lib/review/schema.js +9 -1
- package/dist/lib/safe-path.js +16 -0
- package/dist/lib/spine.js +30 -15
- package/dist/lib/state-machine.js +2 -2
- package/dist/lib/surfaces.js +65 -45
- package/dist/lib/telemetry.js +3 -3
- package/dist/lib/tool-presentation.js +4 -4
- package/dist/lib/untrusted.js +3 -3
- package/dist/lib/validate-crux.js +3 -3
- package/dist/lib/validate-seal.js +26 -7
- package/dist/resources.js +6 -1
- package/dist/server.js +15 -10
- package/dist/tools/amend-dismiss.js +30 -4
- package/dist/tools/candidates.js +1 -1
- package/dist/tools/check-in.js +46 -6
- package/dist/tools/errors.js +18 -4
- package/dist/tools/init-config.js +14 -0
- package/dist/tools/open-decision.js +6 -5
- package/dist/tools/premises.js +29 -9
- package/dist/tools/public-tools.js +37 -21
- package/dist/tools/recall.js +44 -14
- package/dist/tools/recheck.js +65 -11
- package/dist/tools/review.js +6 -6
- package/dist/tools/seal.js +15 -14
- package/dist/tools/settle.js +44 -22
- package/dist/tools/tool-types.js +35 -9
- package/dist/v2/lifecycle-cli.js +23 -8
- package/dist/v2/lifecycle.js +27 -0
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ const KO_ERRORS = {
|
|
|
4
4
|
INVALID_LOCALE: { message: '지원하지 않는 언어입니다.', recovery: 'locale에는 "ko" 또는 "en"을 사용하세요.' },
|
|
5
5
|
ALREADY_CLOSED: { message: '이미 진행 중이거나 닫힌 결정입니다.', recovery: '실제 결과를 기록하려면 argus_resolve를 사용하세요. 닫힌 결정은 다시 열지 않습니다.' },
|
|
6
6
|
CAPTURE_NOT_FOUND: { message: '일치하는 내부 메모를 찾지 못했습니다.', recovery: '전제 문장을 text에 직접 전달하세요.' },
|
|
7
|
-
AMBIGUOUS_REF: { message: '여러
|
|
7
|
+
AMBIGUOUS_REF: { message: '참조가 여러 항목과 일치합니다.', recovery: 'P1 같은 서수(번호)로 정확히 지정하세요. 번호는 argus_patterns view="decision_context"에서 볼 수 있습니다.' },
|
|
8
8
|
PROVENANCE_REQUIRED: { message: '문장의 출처를 확인해야 합니다.', recovery: '사용자가 쓴 문장이면 user_stated, AI가 제기한 문장이면 ai_surfaced와 원문을 전달하세요.' },
|
|
9
9
|
PREMISES_REQUIRED: { message: '추가할 전제가 없습니다.', recovery: 'text, kind, external, load_bearing, source를 포함한 전제 1~5개를 전달하세요.' },
|
|
10
10
|
PREMISE_ID_COLLISION: { message: '다른 전제가 같은 식별자를 사용하고 있습니다.', recovery: '전제 문장을 조금 다르게 표현한 뒤 다시 추가하세요.' },
|
|
@@ -29,13 +29,17 @@ const KO_ERRORS = {
|
|
|
29
29
|
PREMATURE_SETTLE: { message: '아직 확인일이 되지 않았습니다.', recovery: '확인일까지 기다리세요. 일정이 바뀌었다면 outcome="still_pending"에 defer_to로 새 확인일을 전달하면 됩니다.' },
|
|
30
30
|
// ko/en 패리티: 아래 코드들은 en에서만 상세했고 ko는 제네릭 폴백이었다 —
|
|
31
31
|
// 한국어 사용자가 같은 품질의 복구 안내를 받도록 전용 문구를 둔다.
|
|
32
|
-
NO_PRIOR_SEAL: { message: '이 id로 저장된 예측이 없습니다.', recovery: 'argus_predict로
|
|
33
|
-
BAD_CHECK_BY: { message: '확인일이 오늘 이후의 날짜(YYYY-MM-DD)가
|
|
34
|
-
ILLEGAL_TRANSITION: { message: '
|
|
32
|
+
NO_PRIOR_SEAL: { message: '이 id로 저장된 예측이 없습니다.', recovery: 'argus_predict로 나중에 확인할 수 있는 예측과 확인일을 먼저 저장하세요. (id가 argus_settings sync에서 온 "mcp_" 접두사라면 접두사를 뗀 id를 쓰세요.)' },
|
|
33
|
+
BAD_CHECK_BY: { message: '확인일이 오늘 이후의 실제 달력 날짜(YYYY-MM-DD)가 아닙니다 (예: 2026-13-01처럼 없는 달·날짜는 불가).', recovery: '오늘 이후의 올바른 날짜를 YYYY-MM-DD로 다시 전달하세요.' },
|
|
34
|
+
ILLEGAL_TRANSITION: { message: '이 결정에 지금은 할 수 없는 작업입니다 (id 오타이거나, 이미 저장·정산·종료된 상태일 수 있습니다).', recovery: 'argus_patterns view="all"로 id와 현재 상태를 확인하세요. 없는 id면 argus_capture 또는 argus_predict로 새로 시작하세요.' },
|
|
35
|
+
PREMISE_LOCKED: { message: '확인일이 지나 전제를 더는 바꿀 수 없습니다.', recovery: '먼저 argus_resolve로 실제 결과를 기록하세요. 확인일이 온 뒤에는 전제/예측을 고칠 수 없습니다.' },
|
|
36
|
+
ARGUS_DIR_INVALID: { message: 'Argus 기록 경로(argus_dir / ARGUS_DIR)가 올바르지 않습니다.', recovery: '절대 경로여야 하고 ".."을 포함할 수 없습니다. MCP 설정에서 절대 경로(예: C:\\Users\\이름\\.argus, /Users/이름/.argus)로 바꾸거나 ARGUS_DIR을 지워 기본값(~/.argus)을 쓰세요. ${...} 같은 변수는 호스트가 확장하지 못할 수 있습니다.' },
|
|
37
|
+
ARGUS_DIR_UNWRITABLE: { message: 'Argus가 기록 폴더를 만들거나 쓰지 못했습니다.', recovery: 'ARGUS_DIR(또는 argus_dir)을 실제로 있고 쓸 수 있는 폴더로 바꿔 주세요. 실제 드라이브의 절대 경로여야 하고 ".."은 넣을 수 없습니다. 그다음 다시 시도하세요.' },
|
|
38
|
+
EMPTY_PREDICATE: { message: '확인 가능한 예측 문장이 필요합니다 (공백 제외 최소 8자).', recovery: '현실이 참/거짓으로 확인할 수 있는 문장으로 다시 적으세요. 예: "컷오버 다운타임 5분 미만".' },
|
|
35
39
|
ALREADY_SETTLED: { message: '이미 실제 결과가 기록된 결정입니다.', recovery: '영수증은 argus_patterns view="receipt"로 볼 수 있습니다. 새 결정이면 새 id로 여세요.' },
|
|
36
|
-
DECISION_CLOSED: { message: '
|
|
40
|
+
DECISION_CLOSED: { message: '닫힌 결정이라 더 진행할 수 없습니다.', recovery: '필요하면 새 id로 다시 여세요. 닫힌 기록은 그대로 남습니다.' },
|
|
37
41
|
GOALPOST_MOVED: { message: '봉인된 예측 문장은 확인일 전에 바꿀 수 없습니다.', recovery: '일정 변경은 outcome="still_pending"과 defer_to로, 예측 자체가 달라졌다면 새 결정으로 여세요.' },
|
|
38
|
-
NO_SUCH_PREMISE: { message: '해당 번호의 전제를 찾지
|
|
42
|
+
NO_SUCH_PREMISE: { message: '해당 번호의 전제를 찾지 못했습니다 (이 결정에 아직 전제가 없을 수 있습니다).', recovery: 'argus_patterns view="decision_context"로 목록과 번호를 확인하고, 전제가 없으면 argus_capture action="add_context"로 먼저 추가하세요.' },
|
|
39
43
|
WHAT_HAPPENED_REQUIRED: { message: '실제로 일어난 일을 기록해야 합니다.', recovery: '사용자에게 실제 결과를 물어 what_happened에 그대로 전달하세요.' },
|
|
40
44
|
DEFER_DATE_REQUIRED: { message: '다시 확인할 날짜가 필요합니다.', recovery: '사용자에게 날짜를 물어 defer_to에 YYYY-MM-DD로 전달하세요. 더는 중요하지 않다면 argus_capture action="close"를 사용하세요.' },
|
|
41
45
|
NOT_CONNECTED: { message: '이 터미널은 Argus 계정과 연결돼 있지 않습니다.', recovery: '웹 설정에서 동기화 토큰을 발급하고 MCP 설정의 ARGUS_TOKEN에 넣으세요.' },
|
|
@@ -46,10 +50,49 @@ const KO_ERRORS = {
|
|
|
46
50
|
UNKNOWN_TOOL: { message: '알 수 없는 도구입니다.', recovery: 'tools/list에 나온 정확한 도구 이름을 사용하세요.' },
|
|
47
51
|
};
|
|
48
52
|
export const LOCALIZED_ERROR_CODES = new Set(Object.keys(KO_ERRORS));
|
|
53
|
+
// The allowed values for each enum field, so a wrong guess (stakes="medium",
|
|
54
|
+
// outcome="success", …) TEACHES the model the valid set instead of a bare "not
|
|
55
|
+
// allowed". Keyed by the field's LAST path segment (so premises.0.kind → kind).
|
|
56
|
+
const ENUM_HINTS = {
|
|
57
|
+
stakes: 'trivial · low · moderate · high',
|
|
58
|
+
reversibility: 'one_way_door · costly_to_reverse · easily_reversible',
|
|
59
|
+
outcome: 'held · avoided · partial · still_pending · missed',
|
|
60
|
+
outcome_source: 'user_stated',
|
|
61
|
+
view: 'active · all · receipt · decision_context · timeline · reflection',
|
|
62
|
+
action: 'open · add_context · answer_question · keep_question_open · update_fact · change_prediction · close',
|
|
63
|
+
locale: 'ko · en',
|
|
64
|
+
basis: 'judgment · luck · mixed · unsure',
|
|
65
|
+
kind: 'premise · open_question',
|
|
66
|
+
predicate_owner: 'user · ai_surfaced',
|
|
67
|
+
source: 'user_stated · ai_surfaced (update_fact에서는 url · user_stated · host_reported)',
|
|
68
|
+
dismiss_reason: 'became_irrelevant · decided_elsewhere · superseded · user_declined · changed_mind · other',
|
|
69
|
+
};
|
|
70
|
+
const DATE_FIELDS = new Set(['check_by', 'defer_to', 'today_override', 'snooze_until']);
|
|
71
|
+
const TYPE_KO = { string: '문자열', number: '숫자', boolean: '참/거짓', array: '목록', object: '객체', integer: '정수' };
|
|
72
|
+
/** The REAL allowed values, parsed from Zod's own English message
|
|
73
|
+
* ("… expected one of "a"|"b"|"c""). Lets the Korean surface show the ACTUAL
|
|
74
|
+
* per-tool enum instead of a hardcoded guess — ENUM_HINTS is keyed by field
|
|
75
|
+
* NAME, so `action` in argus_settings (status·update·sync) was being told the
|
|
76
|
+
* argus_capture action set. The message carries the truth per call; use it. */
|
|
77
|
+
function enumValuesFromMessage(msg) {
|
|
78
|
+
const m = msg?.match(/expected one of (.+)$/i);
|
|
79
|
+
if (!m)
|
|
80
|
+
return [];
|
|
81
|
+
return (m[1].match(/"([^"]+)"|'([^']+)'/g) ?? []).map((s) => s.replace(/["']/g, ''));
|
|
82
|
+
}
|
|
83
|
+
/** The offending key name(s) from Zod's "Unrecognized key(s): "x"" message, so
|
|
84
|
+
* the Korean surface can NAME what to remove instead of a blank "요청:". */
|
|
85
|
+
function keysFromMessage(msg) {
|
|
86
|
+
if (!msg || !/unrecognized key/i.test(msg))
|
|
87
|
+
return [];
|
|
88
|
+
return (msg.match(/"([^"]+)"|'([^']+)'/g) ?? []).map((s) => s.replace(/["']/g, ''));
|
|
89
|
+
}
|
|
49
90
|
/** Translate one Zod issue into a Korean, actionable reason. Keeps the English
|
|
50
91
|
* argument NAME (models and users see arg names in English), but says in Korean
|
|
51
|
-
* WHY it failed — the piece the generic message threw away.
|
|
52
|
-
|
|
92
|
+
* WHY it failed — the piece the generic message threw away. Field-aware so an id
|
|
93
|
+
* regex failure isn't told to "use YYYY-MM-DD" and an enum lists its values. */
|
|
94
|
+
function koReason(issue, field) {
|
|
95
|
+
const key = field.split('.').pop() || field;
|
|
53
96
|
const unit = issue.origin === 'string' ? '자' : issue.origin === 'array' ? '개' : '';
|
|
54
97
|
switch (issue.code) {
|
|
55
98
|
case 'too_small':
|
|
@@ -65,16 +108,29 @@ function koReason(issue) {
|
|
|
65
108
|
return `항목이 너무 많습니다 (최대 ${issue.maximum}개)`;
|
|
66
109
|
return `너무 큽니다 (최대 ${issue.maximum}${unit})`;
|
|
67
110
|
case 'invalid_type':
|
|
68
|
-
return issue.expected === undefined ? '형식이 올바르지 않습니다' : `필수이거나 형식이 올바르지 않습니다 (${issue.expected} 필요)`;
|
|
111
|
+
return issue.expected === undefined ? '형식이 올바르지 않습니다' : `필수이거나 형식이 올바르지 않습니다 (${TYPE_KO[issue.expected] ?? issue.expected} 형식 필요)`;
|
|
69
112
|
case 'invalid_value':
|
|
70
|
-
case 'invalid_enum_value':
|
|
71
|
-
|
|
113
|
+
case 'invalid_enum_value': {
|
|
114
|
+
const vals = enumValuesFromMessage(issue.message);
|
|
115
|
+
if (vals.length)
|
|
116
|
+
return `허용되지 않는 값입니다 (가능: ${vals.join(' · ')})`;
|
|
117
|
+
const hint = ENUM_HINTS[key];
|
|
118
|
+
return hint ? `허용되지 않는 값입니다 (가능: ${hint})` : '허용되지 않는 값입니다';
|
|
119
|
+
}
|
|
72
120
|
case 'unrecognized_keys':
|
|
73
121
|
return '알 수 없는 항목입니다';
|
|
74
122
|
case 'invalid_format':
|
|
75
123
|
case 'invalid_string':
|
|
76
|
-
|
|
124
|
+
if (key === 'id')
|
|
125
|
+
return '영문·숫자와 . _ - 만 쓸 수 있습니다 (한글·공백·특수문자 불가, 예: "career-move")';
|
|
126
|
+
if (DATE_FIELDS.has(key))
|
|
127
|
+
return 'YYYY-MM-DD 형식의 날짜여야 합니다';
|
|
128
|
+
return '형식이 올바르지 않습니다';
|
|
77
129
|
default:
|
|
130
|
+
// Same text as the invalid_format id case so the two issues the id regex +
|
|
131
|
+
// superRefine both raise dedup to a single line.
|
|
132
|
+
if (key === 'id')
|
|
133
|
+
return '영문·숫자와 . _ - 만 쓸 수 있습니다 (한글·공백·특수문자 불가, 예: "career-move")';
|
|
78
134
|
return '값을 확인해 주세요';
|
|
79
135
|
}
|
|
80
136
|
}
|
|
@@ -82,12 +138,115 @@ function koReason(issue) {
|
|
|
82
138
|
function localizeInvalidInput(fields) {
|
|
83
139
|
if (!fields.length)
|
|
84
140
|
return KO_ERRORS.INVALID_INPUT;
|
|
85
|
-
const
|
|
141
|
+
const seen = new Set();
|
|
142
|
+
const parts = [];
|
|
143
|
+
for (const f of fields) {
|
|
144
|
+
let part;
|
|
145
|
+
if (f.code === 'unrecognized_keys') {
|
|
146
|
+
// The field path is just '(root)'; the useful info is WHICH key. Name it,
|
|
147
|
+
// and drop the meaningless "요청:" prefix that told the user nothing.
|
|
148
|
+
const keys = keysFromMessage(f.message);
|
|
149
|
+
part = keys.length
|
|
150
|
+
? `${keys.map((k) => `"${k}"`).join(', ')}: 이 도구가 받지 않는 항목입니다`
|
|
151
|
+
: '이 도구가 받지 않는 항목입니다';
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const name = f.field === '(root)' ? '요청' : f.field;
|
|
155
|
+
part = `${name}: ${koReason(f, f.field)}`;
|
|
156
|
+
}
|
|
157
|
+
if (seen.has(part))
|
|
158
|
+
continue; // dedup "id: …, id: …" (regex + superRefine both fire)
|
|
159
|
+
seen.add(part);
|
|
160
|
+
parts.push(part);
|
|
161
|
+
if (parts.length >= 4)
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
86
164
|
return {
|
|
87
|
-
message: `입력값이 올바르지
|
|
165
|
+
message: `입력값이 올바르지 않습니다: ${parts.join(', ')}.`,
|
|
88
166
|
recovery: '위에 표시된 인자를 고친 뒤 같은 도구를 다시 호출하세요. 사용자가 정해야 할 값은 추측하지 마세요.',
|
|
89
167
|
};
|
|
90
168
|
}
|
|
169
|
+
/** English mirror of koReason: a plain reason for one Zod issue, so an EN user
|
|
170
|
+
* sees "check_by is required" instead of raw "expected string, received undefined". */
|
|
171
|
+
function enReason(issue) {
|
|
172
|
+
const key = issue.field.split('.').pop() || issue.field;
|
|
173
|
+
switch (issue.code) {
|
|
174
|
+
case 'too_small':
|
|
175
|
+
if (issue.origin === 'string')
|
|
176
|
+
return `is too short (min ${issue.minimum} characters)`;
|
|
177
|
+
if (issue.origin === 'array')
|
|
178
|
+
return `needs at least ${issue.minimum} item${issue.minimum === 1 ? '' : 's'}`;
|
|
179
|
+
return `is too small (min ${issue.minimum})`;
|
|
180
|
+
case 'too_big':
|
|
181
|
+
if (issue.origin === 'string')
|
|
182
|
+
return `is too long (max ${issue.maximum} characters)`;
|
|
183
|
+
if (issue.origin === 'array')
|
|
184
|
+
return `has too many items (max ${issue.maximum})`;
|
|
185
|
+
return `is too big (max ${issue.maximum})`;
|
|
186
|
+
case 'invalid_type':
|
|
187
|
+
return issue.expected ? `is required (expected ${issue.expected})` : 'has the wrong format';
|
|
188
|
+
case 'invalid_value':
|
|
189
|
+
case 'invalid_enum_value': {
|
|
190
|
+
const vals = enumValuesFromMessage(issue.message);
|
|
191
|
+
return vals.length ? `must be one of ${vals.join(', ')}` : 'is not an allowed value';
|
|
192
|
+
}
|
|
193
|
+
case 'invalid_format':
|
|
194
|
+
case 'invalid_string':
|
|
195
|
+
if (key === 'id')
|
|
196
|
+
return 'may use only letters, digits and . _ - (no spaces or other characters, e.g. "career-move")';
|
|
197
|
+
if (DATE_FIELDS.has(key))
|
|
198
|
+
return 'must be a YYYY-MM-DD date';
|
|
199
|
+
return 'has the wrong format';
|
|
200
|
+
default:
|
|
201
|
+
if (key === 'id')
|
|
202
|
+
return 'may use only letters, digits and . _ - (no spaces or other characters, e.g. "career-move")';
|
|
203
|
+
return 'needs checking';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** English INVALID_INPUT that NAMES the offending argument(s), instead of raw
|
|
207
|
+
* Zod ("(root): Unrecognized key", "expected string, received undefined"). */
|
|
208
|
+
function englishInvalidInput(fields) {
|
|
209
|
+
const recovery = 'Fix the named argument(s) and call the same tool again. Do not infer missing user-owned fields.';
|
|
210
|
+
if (!fields.length)
|
|
211
|
+
return { message: 'Invalid input.', recovery };
|
|
212
|
+
const seen = new Set();
|
|
213
|
+
const parts = [];
|
|
214
|
+
for (const f of fields) {
|
|
215
|
+
let part;
|
|
216
|
+
if (f.code === 'unrecognized_keys') {
|
|
217
|
+
const keys = keysFromMessage(f.message);
|
|
218
|
+
part = keys.length
|
|
219
|
+
? `${keys.map((k) => `"${k}"`).join(', ')} ${keys.length === 1 ? "isn't a field" : "aren't fields"} this tool accepts`
|
|
220
|
+
: 'an unexpected field was passed';
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const name = f.field === '(root)' ? 'the request' : f.field;
|
|
224
|
+
part = `${name} ${enReason(f)}`;
|
|
225
|
+
}
|
|
226
|
+
if (seen.has(part))
|
|
227
|
+
continue;
|
|
228
|
+
seen.add(part);
|
|
229
|
+
parts.push(part);
|
|
230
|
+
if (parts.length >= 4)
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
return { message: `Invalid input: ${parts.join('; ')}.`, recovery };
|
|
234
|
+
}
|
|
235
|
+
/** Friendly English for the codes whose handler/guard message otherwise leaks
|
|
236
|
+
* internal machinery (raw Zod, state-machine states). Codes NOT listed keep
|
|
237
|
+
* their handler's already-fine English. */
|
|
238
|
+
const EN_FRIENDLY = {
|
|
239
|
+
ILLEGAL_TRANSITION: {
|
|
240
|
+
message: "This isn't a step you can take on this decision right now (the id may be a typo, or it may already be saved, settled, or closed).",
|
|
241
|
+
recovery: 'Check the id and its state with argus_patterns view="all". If no such id exists, start fresh with argus_capture or argus_predict.',
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
function englishHumanize(code, sc) {
|
|
245
|
+
if (code === 'INVALID_INPUT' && Array.isArray(sc['invalid_fields'])) {
|
|
246
|
+
return englishInvalidInput(sc['invalid_fields']);
|
|
247
|
+
}
|
|
248
|
+
return EN_FRIENDLY[code] ?? null;
|
|
249
|
+
}
|
|
91
250
|
const REPRESENTATIVE_FIELDS = [
|
|
92
251
|
'decision', 'predicate', 'what_happened', 'finding', 'text', 'question',
|
|
93
252
|
'human_judgment', 'note', 'title', 'biggest_worry',
|
|
@@ -106,18 +265,56 @@ function responseLocale(args) {
|
|
|
106
265
|
* user surface even on validation, guard, and rare failure paths.
|
|
107
266
|
*/
|
|
108
267
|
export function localizeToolResult(args, result) {
|
|
109
|
-
if (!result.isError
|
|
268
|
+
if (!result.isError)
|
|
110
269
|
return result;
|
|
111
270
|
const sc = result.structuredContent;
|
|
112
271
|
if (!sc || sc['ok'] !== false)
|
|
113
272
|
return result;
|
|
114
273
|
const code = String(sc['error_code'] ?? 'INTERNAL_ERROR');
|
|
274
|
+
if (responseLocale(args) !== 'ko') {
|
|
275
|
+
// English path: handler prose is already English — humanize ONLY the codes
|
|
276
|
+
// that otherwise leak raw Zod / state-machine internals to the user.
|
|
277
|
+
const en = englishHumanize(code, sc);
|
|
278
|
+
if (!en)
|
|
279
|
+
return result;
|
|
280
|
+
const enLocalized = { ...sc, message: en.message, ...(en.recovery ? { recovery: en.recovery } : {}) };
|
|
281
|
+
result.structuredContent = enLocalized;
|
|
282
|
+
result.content = [{ type: 'text', text: JSON.stringify(enLocalized, null, 2) }];
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
// Some handlers already return a hand-written Korean message (e.g.
|
|
286
|
+
// NOT_FALSIFIABLE: "이건 기분이지 확인 가능한 예측이 아닙니다"). If this code
|
|
287
|
+
// isn't in KO_ERRORS, the generic fallback used to DESTROY that Korean copy.
|
|
288
|
+
// Preserve any message that already contains Hangul instead of overwriting it.
|
|
289
|
+
const existingMsg = typeof sc['message'] === 'string' ? sc['message'] : '';
|
|
290
|
+
const existingRec = typeof sc['recovery'] === 'string' ? sc['recovery'] : '';
|
|
291
|
+
const genericFallback = /[가-힣]/.test(existingMsg)
|
|
292
|
+
? { message: existingMsg, ...(existingRec ? { recovery: existingRec } : {}) }
|
|
293
|
+
: { message: '요청을 처리하지 못했습니다.', recovery: '입력값과 현재 결정 상태를 확인한 뒤 다시 시도하세요.' };
|
|
115
294
|
let copy = code === 'INVALID_INPUT' && Array.isArray(sc['invalid_fields'])
|
|
116
295
|
? localizeInvalidInput(sc['invalid_fields'])
|
|
117
|
-
: KO_ERRORS[code] ??
|
|
118
|
-
|
|
119
|
-
|
|
296
|
+
: KO_ERRORS[code] ?? genericFallback;
|
|
297
|
+
// A handler-authored KOREAN message is at least as specific as the generic
|
|
298
|
+
// map — KO_ERRORS exists to replace ENGLISH copy, not better Korean. Without
|
|
299
|
+
// this, errors.ts's "내부 오류가 발생했습니다: EACCES …" lost its detail to
|
|
300
|
+
// the generic '내부 오류가 발생했습니다.' (1.4.6 backlog: ko detail loss).
|
|
301
|
+
// Quoted spans are stripped BEFORE the Hangul test: an English template that
|
|
302
|
+
// merely embeds the user's Korean predicate ('already sealed: "매출 1억…"')
|
|
303
|
+
// is still English-authored and must still be replaced.
|
|
304
|
+
const authoredKo = (s) => /[가-힣]/.test(s.replace(/"[^"]*"|'[^']*'|「[^」]*」/g, ''));
|
|
305
|
+
if (code !== 'INVALID_INPUT' && authoredKo(existingMsg)) {
|
|
306
|
+
copy = {
|
|
307
|
+
message: existingMsg,
|
|
308
|
+
...(existingRec && authoredKo(existingRec) ? { recovery: existingRec } : copy.recovery ? { recovery: copy.recovery } : {}),
|
|
120
309
|
};
|
|
310
|
+
}
|
|
311
|
+
else if (code === 'INTERNAL_ERROR') {
|
|
312
|
+
// English-authored internal error: carry the diagnostic detail across the
|
|
313
|
+
// language switch instead of discarding it.
|
|
314
|
+
const d = existingMsg.match(/^Internal error: ([\s\S]+)$/);
|
|
315
|
+
if (d)
|
|
316
|
+
copy = { message: `내부 오류가 발생했습니다: ${d[1]}`, ...(copy.recovery ? { recovery: copy.recovery } : {}) };
|
|
317
|
+
}
|
|
121
318
|
// en에만 있던 날짜 상세를 ko에서도 보존 — "언제가 확인일인데?"에 답이 되도록.
|
|
122
319
|
if (code === 'PREMATURE_SETTLE') {
|
|
123
320
|
const m = String(sc['message'] ?? '').match(/check-by (\d{4}-\d{2}-\d{2}), today (\d{4}-\d{2}-\d{2})/);
|
|
@@ -111,7 +111,7 @@ export function evaluateMateriality(prev, next, rule, ctx) {
|
|
|
111
111
|
return { status: 'unchanged', reason: '라벨 동일 (정규화 후)' };
|
|
112
112
|
return {
|
|
113
113
|
status: 'uncertain',
|
|
114
|
-
reason: '비수치/미정규
|
|
114
|
+
reason: '비수치/미정규 라벨. canonical scale를 지정하면 기계 판정 가능 (host 판정 레인)',
|
|
115
115
|
low_confidence: true,
|
|
116
116
|
};
|
|
117
117
|
}
|
|
@@ -120,7 +120,7 @@ export function evaluateMateriality(prev, next, rule, ctx) {
|
|
|
120
120
|
pv = asNumber(prev);
|
|
121
121
|
nv = asNumber(next);
|
|
122
122
|
if (pv === null || nv === null) {
|
|
123
|
-
return { status: 'unchanged', reason: 'non-finite input
|
|
123
|
+
return { status: 'unchanged', reason: 'non-finite input; not comparable' };
|
|
124
124
|
}
|
|
125
125
|
// unit canonicalization before comparing (§3.2)
|
|
126
126
|
if (ctx?.unit_from) {
|
|
@@ -159,7 +159,7 @@ export function evaluateMateriality(prev, next, rule, ctx) {
|
|
|
159
159
|
}
|
|
160
160
|
return {
|
|
161
161
|
status: 'uncertain',
|
|
162
|
-
reason: '부호 전환이나 0의 의미(zero_meaningful)
|
|
162
|
+
reason: '부호 전환이나 0의 의미(zero_meaningful) 미선언. 규칙을 정해주세요',
|
|
163
163
|
low_confidence: true,
|
|
164
164
|
};
|
|
165
165
|
}
|
|
@@ -169,7 +169,7 @@ export function evaluateMateriality(prev, next, rule, ctx) {
|
|
|
169
169
|
if (axis === 'ratio') {
|
|
170
170
|
return {
|
|
171
171
|
status: 'uncertain',
|
|
172
|
-
reason: '값이
|
|
172
|
+
reason: '값이 비율(%)입니다. %p(퍼센트포인트)로 볼지 여집합(100−값) 축으로 볼지 정해주세요',
|
|
173
173
|
low_confidence: true,
|
|
174
174
|
};
|
|
175
175
|
}
|
|
@@ -190,14 +190,14 @@ export function evaluateMateriality(prev, next, rule, ctx) {
|
|
|
190
190
|
}
|
|
191
191
|
return rel >= BIG_MULT
|
|
192
192
|
? { status: 'material', reason: `near-zero ${rel.toFixed(1)}배 이동: ${pv} → ${nv}` }
|
|
193
|
-
: { status: 'uncertain', reason: 'near-zero
|
|
193
|
+
: { status: 'uncertain', reason: 'near-zero 애매. 규칙(delta/safety_floor)을 정해주세요', low_confidence: true };
|
|
194
194
|
}
|
|
195
195
|
// 4b/4c. general scale-free: knife-edge FIRST (exactly-at-threshold is the
|
|
196
196
|
// >= vs > hostage case → uncertain, §8 PCT-08/DATE-09), then relative.
|
|
197
197
|
const rel = Math.abs(nv - pv) / Math.abs(pv);
|
|
198
198
|
const P = REL_DEFAULT;
|
|
199
199
|
if (Math.abs(rel - P) <= EPS) {
|
|
200
|
-
return { status: 'uncertain', reason: `상대변화가 임계 경계에 걸침 (${Math.round(rel * 100)}%)
|
|
200
|
+
return { status: 'uncertain', reason: `상대변화가 임계 경계에 걸침 (${Math.round(rel * 100)}%). 규칙/밴드를 정해주세요`, low_confidence: true };
|
|
201
201
|
}
|
|
202
202
|
if (rel >= P) {
|
|
203
203
|
return { status: 'material', reason: `${Math.round(rel * 100)}% 이동: ${pv} → ${nv}` };
|
|
@@ -213,7 +213,7 @@ function evaluateDeclaredRule(pv, nv, rule, ctx) {
|
|
|
213
213
|
// then it's uncertain, never a manufactured alert. (threshold/band/step/map
|
|
214
214
|
// are axis-agnostic — a crossed line is a crossed line.)
|
|
215
215
|
if (mod?.unit_axis === 'ratio' && (rule.type === 'relative' || rule.type === 'delta')) {
|
|
216
|
-
return { status: 'uncertain', reason: '값이
|
|
216
|
+
return { status: 'uncertain', reason: '값이 비율(%)입니다. %p(퍼센트포인트)로 볼지 여집합(100−값) 축으로 볼지 정해주세요', low_confidence: true };
|
|
217
217
|
}
|
|
218
218
|
// sign-flip modifier (§2.3 / §6.5): dead-band AND, never "always material".
|
|
219
219
|
// A declared sign_flip is decided here (not by the raw delta/relative magnitude).
|
|
@@ -225,7 +225,7 @@ function evaluateDeclaredRule(pv, nv, rule, ctx) {
|
|
|
225
225
|
if (Math.abs(pv) >= floor && Math.abs(nv) >= floor) {
|
|
226
226
|
return mod.zero_meaningful === true
|
|
227
227
|
? { status: 'material', reason: `부호 전환: ${pv} → ${nv}` }
|
|
228
|
-
: { status: 'uncertain', reason: '부호 전환이나 0의 의미(zero_meaningful)
|
|
228
|
+
: { status: 'uncertain', reason: '부호 전환이나 0의 의미(zero_meaningful) 미선언. 규칙을 정해주세요', low_confidence: true };
|
|
229
229
|
}
|
|
230
230
|
return { status: 'unchanged', reason: 'dead-band 안 왕복 = 노이즈' };
|
|
231
231
|
}
|
|
@@ -251,7 +251,7 @@ function evaluateDeclaredRule(pv, nv, rule, ctx) {
|
|
|
251
251
|
// boundary undefined and it matters near the line → uncertain (§1.4)
|
|
252
252
|
const atLine = pv === line || nv === line;
|
|
253
253
|
if (atLine)
|
|
254
|
-
return { status: 'uncertain', reason: 'threshold 경계 도달인데 boundary(inclusive/exclusive)
|
|
254
|
+
return { status: 'uncertain', reason: 'threshold 경계 도달인데 boundary(inclusive/exclusive) 미지정. 정해주세요', low_confidence: true };
|
|
255
255
|
}
|
|
256
256
|
const incl = boundary === 'inclusive';
|
|
257
257
|
const crossed = (() => {
|
|
@@ -291,10 +291,10 @@ function evaluateDeclaredRule(pv, nv, rule, ctx) {
|
|
|
291
291
|
case 'relative': {
|
|
292
292
|
const P = numParam(rule.params['P'], REL_DEFAULT);
|
|
293
293
|
if (pv === 0)
|
|
294
|
-
return { status: 'uncertain', reason: 'relative 기준값 0
|
|
294
|
+
return { status: 'uncertain', reason: 'relative 기준값 0. delta 규칙이 필요합니다', low_confidence: true };
|
|
295
295
|
const rel = Math.abs(nv - pv) / Math.abs(pv);
|
|
296
296
|
if (Math.abs(rel - P) <= EPS) {
|
|
297
|
-
return { status: 'uncertain', reason: `상대변화가 임계 경계에 걸침 (${Math.round(rel * 100)}%)
|
|
297
|
+
return { status: 'uncertain', reason: `상대변화가 임계 경계에 걸침 (${Math.round(rel * 100)}%). 규칙/밴드를 정해주세요`, low_confidence: true };
|
|
298
298
|
}
|
|
299
299
|
return rel >= P
|
|
300
300
|
? dirGate({ status: 'material', reason: `${Math.round(rel * 100)}% 이동: ${pv} → ${nv}` })
|
|
@@ -316,7 +316,7 @@ function evaluateDeclaredRule(pv, nv, rule, ctx) {
|
|
|
316
316
|
// v1: opt-in only, and this numeric path sees just two snapshots — it cannot
|
|
317
317
|
// observe a window. Honest: surface as uncertain (path/volatility not decidable
|
|
318
318
|
// from prev→next alone), never fabricate a peak/crossing verdict (§1.3, §10.5).
|
|
319
|
-
return { status: 'uncertain', reason: 'stateful(경로/변동성)은 두 스냅샷으로 판정
|
|
319
|
+
return { status: 'uncertain', reason: 'stateful(경로/변동성)은 두 스냅샷으로 판정 불가. 관측 이력이 필요합니다', low_confidence: true };
|
|
320
320
|
case 'map':
|
|
321
321
|
// handled in the label path; a numeric map here means mis-typed rule.
|
|
322
322
|
return { status: 'uncertain', reason: 'map 규칙은 라벨 값에만 적용됩니다', low_confidence: true };
|
|
@@ -337,7 +337,7 @@ function evaluateMap(prevLabel, nextLabel, rule) {
|
|
|
337
337
|
}
|
|
338
338
|
return {
|
|
339
339
|
status: 'uncertain',
|
|
340
|
-
reason: `상태 전이이나 등록된 material 상태집합 밖: ${prevLabel} → ${nextLabel}
|
|
340
|
+
reason: `상태 전이이나 등록된 material 상태집합 밖: ${prevLabel} → ${nextLabel}. 규칙 정할지 당신 몫`,
|
|
341
341
|
low_confidence: true,
|
|
342
342
|
};
|
|
343
343
|
}
|
package/dist/lib/receipt.js
CHANGED
|
@@ -64,6 +64,15 @@ fallback) {
|
|
|
64
64
|
null;
|
|
65
65
|
const merged = {
|
|
66
66
|
...base,
|
|
67
|
+
// The ledger is the source of truth for the sealed prediction and its date.
|
|
68
|
+
// A change_prediction (amend) updates the CONTRACT but not the seal-time
|
|
69
|
+
// receipt file on disk, so `base.predicate`/`check_by` can be STALE — the
|
|
70
|
+
// receipt then contradicts every list view (dogfood: receipt printed the
|
|
71
|
+
// pre-amend "8월" predicate while the record showed the amended "9월" one).
|
|
72
|
+
// Prefer the current contract values the settle handler resolved from the
|
|
73
|
+
// fold, so the keepsake can never disagree with the ledger.
|
|
74
|
+
...(fallback?.predicate ? { predicate: fallback.predicate } : {}),
|
|
75
|
+
...(fallback?.check_by ? { check_by: fallback.check_by } : {}),
|
|
67
76
|
settled_at: patch.settled_at,
|
|
68
77
|
what_happened: patch.what_happened,
|
|
69
78
|
outcome: patch.outcome,
|
|
@@ -79,7 +88,14 @@ fallback) {
|
|
|
79
88
|
}
|
|
80
89
|
export function readReceipt(argusDir, id) {
|
|
81
90
|
try {
|
|
82
|
-
|
|
91
|
+
const parsed = JSON.parse(deBom(fs.readFileSync(receiptPath(argusDir, id), 'utf8')));
|
|
92
|
+
// A hand-edited / corrupt receipt could be a primitive, null, or an array;
|
|
93
|
+
// renderReceipt does r.predicate.split(...) unguarded and writeSettleReceipt
|
|
94
|
+
// spreads `base`, so a non-object would crash the render / drop every field.
|
|
95
|
+
// Reject anything but a plain object — a corrupt keepsake degrades, not dies.
|
|
96
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
97
|
+
return null;
|
|
98
|
+
return parsed;
|
|
83
99
|
}
|
|
84
100
|
catch {
|
|
85
101
|
return null;
|