argus-decision-mcp 1.0.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/LICENSE +21 -0
- package/README.md +168 -0
- package/SECURITY.md +35 -0
- package/dist/index.js +13 -0
- package/dist/lib/argus-dir.js +85 -0
- package/dist/lib/atomic-write.js +19 -0
- package/dist/lib/continuity.js +31 -0
- package/dist/lib/deBom.js +3 -0
- package/dist/lib/discipline.js +42 -0
- package/dist/lib/due-note.js +54 -0
- package/dist/lib/elicit.js +30 -0
- package/dist/lib/envelope.js +13 -0
- package/dist/lib/layout.js +32 -0
- package/dist/lib/ledger-append.js +27 -0
- package/dist/lib/ledger-replay.js +295 -0
- package/dist/lib/locale.js +20 -0
- package/dist/lib/log.js +14 -0
- package/dist/lib/numeric-drift.js +32 -0
- package/dist/lib/overfire-gate.js +39 -0
- package/dist/lib/premises.js +153 -0
- package/dist/lib/privacy.js +22 -0
- package/dist/lib/push-account.js +70 -0
- package/dist/lib/receipt.js +83 -0
- package/dist/lib/render-receipt.js +144 -0
- package/dist/lib/resolve-contract.js +17 -0
- package/dist/lib/resolve-today.js +47 -0
- package/dist/lib/review/ids.js +29 -0
- package/dist/lib/review/index.js +18 -0
- package/dist/lib/review/ingest.js +294 -0
- package/dist/lib/review/lenses.js +132 -0
- package/dist/lib/review/prompts.js +155 -0
- package/dist/lib/review/render.js +77 -0
- package/dist/lib/review/reviewability.js +83 -0
- package/dist/lib/review/routing.js +147 -0
- package/dist/lib/review/schema.js +36 -0
- package/dist/lib/safe-path.js +70 -0
- package/dist/lib/spine.js +79 -0
- package/dist/lib/state-machine.js +80 -0
- package/dist/lib/surfaces.js +106 -0
- package/dist/lib/validate-crux.js +35 -0
- package/dist/lib/validate-seal.js +48 -0
- package/dist/prompts.js +72 -0
- package/dist/resources.js +122 -0
- package/dist/server.js +107 -0
- package/dist/tools/amend-dismiss.js +90 -0
- package/dist/tools/check-in.js +158 -0
- package/dist/tools/errors.js +23 -0
- package/dist/tools/index.js +14 -0
- package/dist/tools/init-config.js +99 -0
- package/dist/tools/open-decision.js +128 -0
- package/dist/tools/premises.js +209 -0
- package/dist/tools/recall.js +157 -0
- package/dist/tools/recheck.js +147 -0
- package/dist/tools/review.js +182 -0
- package/dist/tools/seal.js +152 -0
- package/dist/tools/settle.js +126 -0
- package/dist/tools/sync.js +129 -0
- package/dist/tools/tool-types.js +32 -0
- package/package.json +64 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lens library — 9 MVP lenses (design doc §"Lens Library"). A lens is a
|
|
3
|
+
* structured review unit, NOT a prompt fragment: it declares what it applies
|
|
4
|
+
* to, what it asks, and which sentences count as failure (generic advice).
|
|
5
|
+
*
|
|
6
|
+
* "Argus의 품질은 좋은 lens를 많이 만드는 것보다 맞는 lens를 고르는 것에서 나온다."
|
|
7
|
+
* So keep this list small and let routing.ts choose.
|
|
8
|
+
*/
|
|
9
|
+
export const LENS_VERSION = '1';
|
|
10
|
+
export const LENSES = {
|
|
11
|
+
core_question: {
|
|
12
|
+
id: 'core_question',
|
|
13
|
+
version: LENS_VERSION,
|
|
14
|
+
label: '핵심 질문',
|
|
15
|
+
applies_to: {},
|
|
16
|
+
purpose: '이 문서가 겉으로 묻는 질문과 실제로 결정해야 하는 질문이 같은지 가른다.',
|
|
17
|
+
input_requirements: ['core_question', 'decision_points'],
|
|
18
|
+
review_questions: [
|
|
19
|
+
'이 문서가 진짜로 결정하려는 것은 무엇인가?',
|
|
20
|
+
'겉 질문과 실제 결정이 다른가?',
|
|
21
|
+
],
|
|
22
|
+
failure_modes: ['목표를 명확히 하세요', '무엇을 원하는지 정의하세요'],
|
|
23
|
+
},
|
|
24
|
+
claim_evidence: {
|
|
25
|
+
id: 'claim_evidence',
|
|
26
|
+
version: LENS_VERSION,
|
|
27
|
+
label: '주장-근거',
|
|
28
|
+
applies_to: {},
|
|
29
|
+
purpose: '핵심 주장마다 근거가 원문 안에 있는지, 아니면 추정인지 가른다.',
|
|
30
|
+
input_requirements: ['main_claims', 'evidence_items'],
|
|
31
|
+
review_questions: [
|
|
32
|
+
'이 주장의 근거가 문서 안에 있는가, 밖에서 끌어온 추정인가?',
|
|
33
|
+
'근거 없이 단정한 수치/사실은 어느 것인가?',
|
|
34
|
+
],
|
|
35
|
+
failure_modes: ['근거를 더 조사하세요', '데이터를 보강하세요'],
|
|
36
|
+
},
|
|
37
|
+
hidden_assumption: {
|
|
38
|
+
id: 'hidden_assumption',
|
|
39
|
+
version: LENS_VERSION,
|
|
40
|
+
label: '숨은 가정',
|
|
41
|
+
applies_to: {},
|
|
42
|
+
purpose: '문서가 말하지 않았지만 의존하는 가정과, 그것이 틀렸을 때 무너지는 것을 짚는다.',
|
|
43
|
+
input_requirements: ['assumptions'],
|
|
44
|
+
review_questions: [
|
|
45
|
+
'이 결론이 성립하려면 참이어야 하는, 말하지 않은 가정은 무엇인가?',
|
|
46
|
+
'그 가정이 틀리면 무엇이 무너지는가?',
|
|
47
|
+
],
|
|
48
|
+
failure_modes: ['가정을 검토하세요', '리스크를 고려하세요'],
|
|
49
|
+
},
|
|
50
|
+
human_judgment: {
|
|
51
|
+
id: 'human_judgment',
|
|
52
|
+
version: LENS_VERSION,
|
|
53
|
+
label: '사람이 판단할 것',
|
|
54
|
+
applies_to: {},
|
|
55
|
+
purpose: 'AI나 문서가 대신 결정하면 안 되는, 사람이 직접 lean을 가져야 하는 지점을 분리한다.',
|
|
56
|
+
input_requirements: ['decision_points'],
|
|
57
|
+
review_questions: [
|
|
58
|
+
'이 문서에서 사람만 판단할 수 있는 항목은 무엇인가?',
|
|
59
|
+
'왜 이것은 모델이나 근거로 대체할 수 없는가?',
|
|
60
|
+
],
|
|
61
|
+
failure_modes: ['신중하게 결정하세요', '전문가와 상의하세요'],
|
|
62
|
+
},
|
|
63
|
+
stakeholder_objection: {
|
|
64
|
+
id: 'stakeholder_objection',
|
|
65
|
+
version: LENS_VERSION,
|
|
66
|
+
label: '이해관계자 반론',
|
|
67
|
+
applies_to: { min_stakes: 'medium' },
|
|
68
|
+
purpose: '핵심 의사결정자가 이 문서의 특정 claim에 가장 먼저 던질 반론을 미리 꺼낸다.',
|
|
69
|
+
input_requirements: ['stakeholders', 'main_claims'],
|
|
70
|
+
review_questions: [
|
|
71
|
+
'예산/승인을 쥔 사람이 어느 claim에서 먼저 "근거가 뭐냐"고 물을까?',
|
|
72
|
+
'그 반론은 문서의 어느 문장에 걸리는가?',
|
|
73
|
+
],
|
|
74
|
+
failure_modes: ['이해관계자를 고려하세요', '반대 의견에 대비하세요'],
|
|
75
|
+
},
|
|
76
|
+
execution_risk: {
|
|
77
|
+
id: 'execution_risk',
|
|
78
|
+
version: LENS_VERSION,
|
|
79
|
+
label: '실행 리스크',
|
|
80
|
+
applies_to: {},
|
|
81
|
+
purpose: '실행 단계에서 막힐 dependency, sequencing, owner gap을 문서 안에서 찾는다.',
|
|
82
|
+
input_requirements: ['decision_points', 'tradeoffs'],
|
|
83
|
+
review_questions: [
|
|
84
|
+
'실행 순서상 먼저 풀리지 않으면 막히는 dependency는 무엇인가?',
|
|
85
|
+
'누가 책임지는지 비어 있는 지점은 어디인가?',
|
|
86
|
+
],
|
|
87
|
+
failure_modes: ['실행 계획을 세우세요', '일정을 관리하세요'],
|
|
88
|
+
},
|
|
89
|
+
reversibility: {
|
|
90
|
+
id: 'reversibility',
|
|
91
|
+
version: LENS_VERSION,
|
|
92
|
+
label: '되돌릴 수 있는가',
|
|
93
|
+
applies_to: { document_type: ['rfc', 'adr', 'strategy_memo', 'proposal'] },
|
|
94
|
+
purpose: '이 결정이 되돌릴 수 있는지, 되돌릴 수 없다면 어떤 proof가 더 필요한지 가른다.',
|
|
95
|
+
input_requirements: ['decision_points'],
|
|
96
|
+
review_questions: [
|
|
97
|
+
'이 결정은 되돌릴 수 있는가, 일방향 문인가?',
|
|
98
|
+
'되돌릴 수 없다면 지금 부족한 proof는 무엇인가?',
|
|
99
|
+
],
|
|
100
|
+
failure_modes: ['위험을 관리하세요', '신중히 진행하세요'],
|
|
101
|
+
},
|
|
102
|
+
falsifiable_followup: {
|
|
103
|
+
id: 'falsifiable_followup',
|
|
104
|
+
version: LENS_VERSION,
|
|
105
|
+
label: '반증 가능한 후속',
|
|
106
|
+
applies_to: {},
|
|
107
|
+
purpose: '나중에 현실이 pass/fail로 답할 수 있는 predicate를 1-3개 만든다.',
|
|
108
|
+
input_requirements: ['core_question', 'main_claims'],
|
|
109
|
+
review_questions: [
|
|
110
|
+
'언제 무엇을 보면 이 판단이 맞았다/틀렸다고 할 수 있는가?',
|
|
111
|
+
'pass 조건과 fail 조건, check-by는 무엇인가?',
|
|
112
|
+
],
|
|
113
|
+
failure_modes: ['결과를 추적하세요', '지표를 모니터링하세요'],
|
|
114
|
+
},
|
|
115
|
+
deck_narrative: {
|
|
116
|
+
id: 'deck_narrative',
|
|
117
|
+
version: LENS_VERSION,
|
|
118
|
+
label: '덱 설득 흐름',
|
|
119
|
+
applies_to: { deck_only: true },
|
|
120
|
+
purpose: 'deck이 어떤 순서로 청중을 설득하는지, 핵심 ask가 언제 드러나고 어디서 논리가 점프하는지 본다.',
|
|
121
|
+
input_requirements: ['main_claims'],
|
|
122
|
+
review_questions: [
|
|
123
|
+
'첫 3장 안에 핵심 질문과 ask가 드러나는가?',
|
|
124
|
+
'핵심 claim이 처음 등장하는 슬라이드와 근거를 받는 슬라이드가 연결되는가?',
|
|
125
|
+
],
|
|
126
|
+
failure_modes: ['슬라이드를 정리하세요', '스토리를 다듬으세요'],
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
export function getLens(id) {
|
|
130
|
+
return LENSES[id];
|
|
131
|
+
}
|
|
132
|
+
export const ALL_LENS_IDS = Object.keys(LENSES);
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the review pipeline's prompts (CLAUDE.md
|
|
3
|
+
* §"Single Source of Truth for Prompts"). Each pipeline stage has a distinct
|
|
4
|
+
* role + output schema (design doc §"LLM Handoff Protocol"):
|
|
5
|
+
* 1. Extraction → profile + Document Judgment Map (no evaluation)
|
|
6
|
+
* 2. Lens Review → one lens's findings only (anchored, no global conclusion)
|
|
7
|
+
* 3. Synthesis → compress into receipt fields (no "proceed" tilt)
|
|
8
|
+
*
|
|
9
|
+
* Hard rules baked into every prompt:
|
|
10
|
+
* - cite the source by unit_id (anchors) — an unanchored finding is worthless.
|
|
11
|
+
* - no generic advice ("리스크를 고려하세요") — must reference a specific claim.
|
|
12
|
+
* - never decide for the user; never invent facts not in the units.
|
|
13
|
+
* - Korean output, pure JSON.
|
|
14
|
+
*/
|
|
15
|
+
const SPINE = `너는 Argus의 판단 검수기다. 사용자의 결정을 대신 내리지 않는다.
|
|
16
|
+
- 절대 "이 전략은 틀렸다", "진행하세요" 같은 평결을 내지 않는다.
|
|
17
|
+
- 모든 지적은 문서의 특정 문장에 연결한다. 근거 위치 없는 지적은 내지 않는다.
|
|
18
|
+
- 사람이 읽는 문장(title·detail·rationale)에서는 위치를 사람이 읽는 표현으로만 지칭한다
|
|
19
|
+
(예: "결론 섹션의 착수 문장", "slide 4의 시장규모 주장", "3번째 문단"). unit_id 같은 내부
|
|
20
|
+
식별자(u_...)를 문장에 절대 노출하지 않는다. unit_id는 오직 unit_ids 배열에만 넣는다.
|
|
21
|
+
- "리스크를 고려하세요", "더 조사하세요" 같은 일반론은 금지한다. 무엇을 확인할지 구체적으로 쓴다.
|
|
22
|
+
- 원문에 없는 사실을 지어내지 않는다.
|
|
23
|
+
- 출력은 순수 JSON. 마크다운/설명 없이 { 로 시작해 } 로 끝난다. 한국어로 쓴다.`;
|
|
24
|
+
export function renderUnits(units, limit) {
|
|
25
|
+
return units
|
|
26
|
+
.slice(0, limit)
|
|
27
|
+
.map((u) => {
|
|
28
|
+
const a = u.source_anchor;
|
|
29
|
+
const loc = a.slide !== undefined
|
|
30
|
+
? `slide ${a.slide}`
|
|
31
|
+
: a.section_path?.length
|
|
32
|
+
? a.section_path.join(' › ')
|
|
33
|
+
: a.line_start !== undefined
|
|
34
|
+
? `L${a.line_start}`
|
|
35
|
+
: '';
|
|
36
|
+
return `[${u.unit_id}] (${u.kind}${loc ? ' · ' + loc : ''}) ${u.text}`;
|
|
37
|
+
})
|
|
38
|
+
.join('\n');
|
|
39
|
+
}
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// 1. Extraction: Canonical Artifact → profile + Document Judgment Map
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
export function buildExtractionPrompt(units, ctx, unitLimit) {
|
|
44
|
+
const concerns = ctx.concerns?.length ? `사용자 관심사: ${ctx.concerns.join(', ')}.` : '';
|
|
45
|
+
const hints = [
|
|
46
|
+
ctx.audience_hint && `대상 독자 힌트: ${ctx.audience_hint}`,
|
|
47
|
+
ctx.decision_wanted && `얻으려는 결정: ${ctx.decision_wanted}`,
|
|
48
|
+
ctx.biggest_worry && `가장 불안한 부분: ${ctx.biggest_worry}`,
|
|
49
|
+
]
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.join('\n');
|
|
52
|
+
const system = `${SPINE}
|
|
53
|
+
|
|
54
|
+
이번 단계는 "추출"이다. 평가/비판/추천은 하지 않는다. 문서를 판단 가능한 지도로만 바꾼다.
|
|
55
|
+
document_type, intent, audience, stakes, artifact_maturity를 추론하고, 확신이 낮으면 source_confidence를 낮게 준다.
|
|
56
|
+
각 항목은 반드시 관련 unit_id 배열을 포함한다.`;
|
|
57
|
+
const user = `아래는 검수할 문서의 단위(unit)들이다. 각 줄은 [unit_id] (종류 · 위치) 텍스트 형식이다.
|
|
58
|
+
|
|
59
|
+
${renderUnits(units, unitLimit)}
|
|
60
|
+
|
|
61
|
+
${hints}
|
|
62
|
+
${concerns}
|
|
63
|
+
|
|
64
|
+
다음 JSON을 출력하라:
|
|
65
|
+
{
|
|
66
|
+
"profile": { "document_type": "...", "intent": "...", "audience": "...", "stakes": "low|medium|high", "artifact_maturity": "...", "source_confidence": 0.0 },
|
|
67
|
+
"core_question": "이 문서가 실제로 결정해야 하는 질문",
|
|
68
|
+
"explicit_recommendation": "문서가 겉으로 미는 결론(없으면 생략)",
|
|
69
|
+
"implicit_recommendation": "은연중 미는 결론(없으면 생략)",
|
|
70
|
+
"main_claims": [ { "text": "...", "status": "supported|weak|unsupported|human_check|contradicted", "unit_ids": ["..."], "rationale": "이 상태로 판단한 근거(원문 기준)", "evidence_needed": "이 주장을 확정하려면 무엇을 확인해야 하는가(없으면 생략)", "fix_suggestion": "이 문장 자체를 어떻게 보강할지 한 줄(선택, 없으면 생략)" } ],
|
|
71
|
+
"evidence_items": [ { "text": "...", "unit_ids": ["..."], "kind": "internal|external_cited|asserted" } ],
|
|
72
|
+
"assumptions": [ { "text": "말하지 않은 가정", "unit_ids": ["..."], "if_false": "틀리면 무너지는 것" } ],
|
|
73
|
+
"tradeoffs": [ { "text": "...", "unit_ids": ["..."] } ],
|
|
74
|
+
"stakeholders": [ { "role": "...", "likely_objection": "...", "unit_ids": ["..."] } ],
|
|
75
|
+
"open_questions": [ { "text": "...", "unit_ids": ["..."] } ],
|
|
76
|
+
"decision_points": [ { "text": "...", "human_only": true, "unit_ids": ["..."] } ],
|
|
77
|
+
"missing_sections": [ { "label": "...", "why_it_matters": "..." } ]
|
|
78
|
+
}`;
|
|
79
|
+
return { system, user };
|
|
80
|
+
}
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// 2. Lens Review: one lens → findings only
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
export function buildLensPrompt(lens, mapSummary, units, unitLimit) {
|
|
85
|
+
const system = `${SPINE}
|
|
86
|
+
|
|
87
|
+
너는 "${lens.label}" 렌즈다. 목적: ${lens.purpose}
|
|
88
|
+
이 렌즈의 검토 질문:
|
|
89
|
+
${lens.review_questions.map((q) => `- ${q}`).join('\n')}
|
|
90
|
+
|
|
91
|
+
금지된 출력(이런 문장을 내면 실패로 간주한다):
|
|
92
|
+
${lens.failure_modes.map((f) => `- "${f}"`).join('\n')}
|
|
93
|
+
|
|
94
|
+
너의 렌즈에 해당하는 finding만 낸다. 전체 결론이나 다른 렌즈의 영역은 건드리지 않는다.
|
|
95
|
+
finding이 없으면 빈 배열을 낸다(억지로 만들지 않는다).`;
|
|
96
|
+
const user = `문서 판단 지도 요약:
|
|
97
|
+
${mapSummary}
|
|
98
|
+
|
|
99
|
+
원문 단위:
|
|
100
|
+
${renderUnits(units, unitLimit)}
|
|
101
|
+
|
|
102
|
+
다음 JSON을 출력하라:
|
|
103
|
+
{
|
|
104
|
+
"findings": [
|
|
105
|
+
{
|
|
106
|
+
"title": "특정 claim에 걸린 짧은 제목 (예: slide 4 시장규모 주장에 근거 없음)",
|
|
107
|
+
"detail": "무엇이 문제인지 원문 기준으로",
|
|
108
|
+
"severity": "minor|caution|critical",
|
|
109
|
+
"confidence": "low|medium|high",
|
|
110
|
+
"suggested_action": "무엇을 확인/보완할지 구체적으로 (일반론 금지)",
|
|
111
|
+
"unit_ids": ["관련 unit_id (필수, 하나 이상)"]
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}`;
|
|
115
|
+
return { system, user };
|
|
116
|
+
}
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// 3. Synthesis: lens findings + map → receipt-level fields
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
export function buildSynthesisPrompt(mapSummary, findingsSummary, ctx, today) {
|
|
121
|
+
const system = `${SPINE}
|
|
122
|
+
|
|
123
|
+
이번 단계는 "종합"이다. 렌즈 결과를 Judgment Receipt 필드로 압축한다.
|
|
124
|
+
- current_heading은 중립적 방향 한 줄이다. "진행하세요" 같은 평결이 아니다.
|
|
125
|
+
- judgment_obligations는 사람이 직접 판단해야 하는 항목이다. AI가 대신 결론내지 않는다.
|
|
126
|
+
- 사용자가 결론을 냈다고 말하지 않는다.
|
|
127
|
+
- follow-up predicate는 나중에 현실이 pass/fail로 답할 수 있어야 한다. check_by는 ${today} 이후의 미래 날짜(YYYY-MM-DD)로 제안한다.`;
|
|
128
|
+
const worry = ctx.biggest_worry ? `사용자가 가장 불안해한 부분: ${ctx.biggest_worry}` : '';
|
|
129
|
+
const user = `문서 판단 지도 요약:
|
|
130
|
+
${mapSummary}
|
|
131
|
+
|
|
132
|
+
렌즈 finding 요약:
|
|
133
|
+
${findingsSummary}
|
|
134
|
+
${worry}
|
|
135
|
+
|
|
136
|
+
다음 JSON을 출력하라:
|
|
137
|
+
{
|
|
138
|
+
"core_question": "한 문장으로 다듬은 핵심 판단 질문",
|
|
139
|
+
"current_heading": "지금 이 문서의 중립적 방향 한 줄 (평결 아님)",
|
|
140
|
+
"judgment_obligations": [
|
|
141
|
+
{
|
|
142
|
+
"statement": "사람이 책임지고 판단해야 할 항목",
|
|
143
|
+
"owner": "사용자",
|
|
144
|
+
"why_human": "왜 모델/근거로 대체할 수 없는가",
|
|
145
|
+
"decision_needed_by": "언제까지 (없으면 생략)",
|
|
146
|
+
"evidence_needed": "무엇을 보면 판단할 수 있는가",
|
|
147
|
+
"unit_ids": ["..."]
|
|
148
|
+
}
|
|
149
|
+
],
|
|
150
|
+
"followups": [
|
|
151
|
+
{ "predicate": "현실이 답할 예측", "pass_condition": "맞았다고 볼 조건", "fail_condition": "틀렸다고 볼 조건", "check_by": "YYYY-MM-DD" }
|
|
152
|
+
]
|
|
153
|
+
}`;
|
|
154
|
+
return { system, user };
|
|
155
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a Judgment Receipt as shareable markdown (design doc §"receipt export
|
|
3
|
+
* markdown"). Same object powers the webapp view and the MCP output, so this is
|
|
4
|
+
* the single text projection. It shows findings + obligations + the neutral
|
|
5
|
+
* heading — never an AI verdict on the decision.
|
|
6
|
+
*/
|
|
7
|
+
export function receiptToMarkdown(r) {
|
|
8
|
+
const L = [];
|
|
9
|
+
L.push(`# Judgment Receipt — ${r.source_title}`);
|
|
10
|
+
L.push('');
|
|
11
|
+
L.push(`- 상태: ${r.state} · 검수 가능성: ${r.reviewability.score}/100`);
|
|
12
|
+
L.push(`- 문서 유형: ${r.profile.document_type} · 이해관계: ${r.profile.stakes}`);
|
|
13
|
+
// Coverage travels with the shared receipt — a partial review must never read
|
|
14
|
+
// as a full one, even when copied out of the app.
|
|
15
|
+
if (r.coverage && r.coverage.band !== 'full' && r.coverage.notes.length) {
|
|
16
|
+
L.push(`- 검수 범위: ${r.coverage.notes.join(' ')}`);
|
|
17
|
+
}
|
|
18
|
+
L.push('');
|
|
19
|
+
L.push('## 핵심 판단');
|
|
20
|
+
L.push(r.core_question || '(핵심 질문 미검출)');
|
|
21
|
+
L.push('');
|
|
22
|
+
if (r.judgment_obligations.length) {
|
|
23
|
+
L.push('## 사람이 직접 판단해야 할 것');
|
|
24
|
+
r.judgment_obligations.forEach((o, i) => {
|
|
25
|
+
L.push(`${i + 1}. ${o.statement}`);
|
|
26
|
+
if (o.why_human)
|
|
27
|
+
L.push(` - 왜 사람인가: ${o.why_human}`);
|
|
28
|
+
if (o.evidence_needed)
|
|
29
|
+
L.push(` - 확인할 근거: ${o.evidence_needed}`);
|
|
30
|
+
});
|
|
31
|
+
L.push('');
|
|
32
|
+
}
|
|
33
|
+
const top = r.findings.slice(0, 5);
|
|
34
|
+
if (top.length) {
|
|
35
|
+
L.push('## 주요 발견');
|
|
36
|
+
top.forEach((f) => {
|
|
37
|
+
L.push(`- [${f.severity}] ${f.title}${anchorHint(f.anchors)}`);
|
|
38
|
+
if (f.suggested_action)
|
|
39
|
+
L.push(` - 확인: ${f.suggested_action}`);
|
|
40
|
+
});
|
|
41
|
+
L.push('');
|
|
42
|
+
}
|
|
43
|
+
if (r.hidden_assumptions.length) {
|
|
44
|
+
L.push('## 그대로 진행하면 위험한 가정');
|
|
45
|
+
r.hidden_assumptions.slice(0, 3).forEach((a) => {
|
|
46
|
+
L.push(`- ${a.text}${a.if_false ? ` → 틀리면: ${a.if_false}` : ''}`);
|
|
47
|
+
});
|
|
48
|
+
L.push('');
|
|
49
|
+
}
|
|
50
|
+
if (r.falsifiable_followups.length) {
|
|
51
|
+
L.push('## 현실이 답할 후속 예측');
|
|
52
|
+
r.falsifiable_followups.forEach((f) => {
|
|
53
|
+
L.push(`- ${f.predicate} (확인일 ${f.check_by})`);
|
|
54
|
+
if (f.pass_condition)
|
|
55
|
+
L.push(` - 맞음: ${f.pass_condition}`);
|
|
56
|
+
if (f.fail_condition)
|
|
57
|
+
L.push(` - 틀림: ${f.fail_condition}`);
|
|
58
|
+
});
|
|
59
|
+
L.push('');
|
|
60
|
+
}
|
|
61
|
+
L.push('---');
|
|
62
|
+
L.push(`적용 렌즈: ${r.routing.selected.join(', ')}`);
|
|
63
|
+
L.push('AI는 이 결정을 대신 판단하지 않았습니다. 판단은 당신의 몫입니다.');
|
|
64
|
+
return L.join('\n');
|
|
65
|
+
}
|
|
66
|
+
function anchorHint(anchors) {
|
|
67
|
+
const a = anchors[0];
|
|
68
|
+
if (!a)
|
|
69
|
+
return '';
|
|
70
|
+
if (a.slide !== undefined)
|
|
71
|
+
return ` (slide ${a.slide})`;
|
|
72
|
+
if (a.section_path?.length)
|
|
73
|
+
return ` (${a.section_path.join(' › ')})`;
|
|
74
|
+
if (a.line_start !== undefined)
|
|
75
|
+
return ` (L${a.line_start})`;
|
|
76
|
+
return '';
|
|
77
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reviewability scoring (design doc §"Reviewability Score가 필요하다").
|
|
3
|
+
*
|
|
4
|
+
* Not every document can be reviewed to the same depth. Argus judges
|
|
5
|
+
* reviewability FIRST so it never speaks confidently over a scanned PDF or an
|
|
6
|
+
* image-only deck. Bands drive the UX:
|
|
7
|
+
* 80–100 normal · 60–79 caveated · 40–59 limited · <40 no full receipt.
|
|
8
|
+
*
|
|
9
|
+
* The score is computed in two passes: a pre-review estimate from the artifact
|
|
10
|
+
* alone (extraction / structure / anchor coverage), then finalized after the
|
|
11
|
+
* judgment map exists (decision clarity / evidence availability).
|
|
12
|
+
*/
|
|
13
|
+
import { reviewabilityBand, } from './schema.js';
|
|
14
|
+
const EXTRACTION_WEIGHT = {
|
|
15
|
+
high: 100,
|
|
16
|
+
medium: 70,
|
|
17
|
+
low: 35,
|
|
18
|
+
unsupported: 0,
|
|
19
|
+
};
|
|
20
|
+
export function scoreReviewability(artifact, map) {
|
|
21
|
+
const reasons = [];
|
|
22
|
+
// 1. extraction
|
|
23
|
+
const extraction = EXTRACTION_WEIGHT[artifact.extraction_quality];
|
|
24
|
+
if (artifact.extraction_quality === 'unsupported') {
|
|
25
|
+
reasons.push('텍스트를 추출하지 못해 전체 검수가 어렵습니다.');
|
|
26
|
+
}
|
|
27
|
+
else if (artifact.extraction_quality === 'low') {
|
|
28
|
+
reasons.push('텍스트 추출 품질이 낮습니다.');
|
|
29
|
+
}
|
|
30
|
+
// 2. structure — headings/sections/slides give the reviewer scaffolding
|
|
31
|
+
const s = artifact.detected_structure;
|
|
32
|
+
const structuralUnits = (s.heading_count ?? 0) + (s.slide_count ?? 0) + (s.section_count ?? 0);
|
|
33
|
+
const structure = clamp(structuralUnits === 0 ? 40 : 60 + Math.min(40, structuralUnits * 8));
|
|
34
|
+
if (structuralUnits === 0 && artifact.units.length > 0) {
|
|
35
|
+
reasons.push('구조(제목/섹션)가 없어 근거 위치를 짚기 어렵습니다.');
|
|
36
|
+
}
|
|
37
|
+
// 3. anchor coverage — share of units that carry a usable anchor
|
|
38
|
+
const anchored = artifact.units.filter((u) => hasAnchor(u.source_anchor)).length;
|
|
39
|
+
const anchor_coverage = artifact.units.length === 0 ? 0 : Math.round((anchored / artifact.units.length) * 100);
|
|
40
|
+
// 4/5. decision clarity + evidence availability (need the map)
|
|
41
|
+
let decision_clarity = 50;
|
|
42
|
+
let evidence_availability = 50;
|
|
43
|
+
if (map) {
|
|
44
|
+
const hasCore = !!map.core_question && map.core_question.length > 8;
|
|
45
|
+
const decisionPoints = map.decision_points.length;
|
|
46
|
+
decision_clarity = clamp((hasCore ? 55 : 20) + Math.min(45, decisionPoints * 15));
|
|
47
|
+
if (!hasCore)
|
|
48
|
+
reasons.push('문서에서 결정할 질문이 뚜렷하지 않습니다.');
|
|
49
|
+
const claims = map.main_claims.length || 1;
|
|
50
|
+
const withEvidence = map.main_claims.filter((c) => c.anchors.length > 0 || c.status === 'supported').length;
|
|
51
|
+
evidence_availability = clamp(Math.round((withEvidence / claims) * 100));
|
|
52
|
+
}
|
|
53
|
+
const score = Math.round(extraction * 0.35 +
|
|
54
|
+
structure * 0.15 +
|
|
55
|
+
anchor_coverage * 0.15 +
|
|
56
|
+
decision_clarity * 0.2 +
|
|
57
|
+
evidence_availability * 0.15);
|
|
58
|
+
const band = reviewabilityBand(score);
|
|
59
|
+
if (band === 'insufficient') {
|
|
60
|
+
reasons.push('전체 receipt 대신 "무엇이 빠졌는지"를 먼저 보여줍니다.');
|
|
61
|
+
}
|
|
62
|
+
else if (band === 'limited') {
|
|
63
|
+
reasons.push('제한적으로만 검수하고 부족한 맥락을 요청합니다.');
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
score,
|
|
67
|
+
extraction,
|
|
68
|
+
structure,
|
|
69
|
+
decision_clarity,
|
|
70
|
+
evidence_availability,
|
|
71
|
+
anchor_coverage,
|
|
72
|
+
reasons,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function hasAnchor(a) {
|
|
76
|
+
return (a.line_start !== undefined ||
|
|
77
|
+
a.page !== undefined ||
|
|
78
|
+
a.slide !== undefined ||
|
|
79
|
+
(a.section_path !== undefined && a.section_path.length > 0));
|
|
80
|
+
}
|
|
81
|
+
function clamp(n) {
|
|
82
|
+
return Math.max(0, Math.min(100, Math.round(n)));
|
|
83
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic lens routing (design doc §"Dynamic Lens Routing").
|
|
3
|
+
*
|
|
4
|
+
* "Argus의 품질은 좋은 lens를 많이 만드는 것보다 맞는 lens를 고르는 것에서 나온다."
|
|
5
|
+
* Routing is deterministic and testable: it reads the profile + the deck flag +
|
|
6
|
+
* the user's concern chips and returns which lenses run, which are skipped (with
|
|
7
|
+
* a reason), and a user-facing disclosure line — the routing is never hidden.
|
|
8
|
+
*/
|
|
9
|
+
import { LENSES, ALL_LENS_IDS } from './lenses.js';
|
|
10
|
+
const STAKES_ORDER = { low: 0, medium: 1, high: 2 };
|
|
11
|
+
/** Lenses that always run — the judgment spine of any review. */
|
|
12
|
+
const BASE = [
|
|
13
|
+
'core_question',
|
|
14
|
+
'claim_evidence',
|
|
15
|
+
'hidden_assumption',
|
|
16
|
+
'human_judgment',
|
|
17
|
+
'falsifiable_followup',
|
|
18
|
+
];
|
|
19
|
+
const CONCERN_TO_LENS = {
|
|
20
|
+
strategic_fit: ['core_question', 'hidden_assumption'],
|
|
21
|
+
evidence: ['claim_evidence'],
|
|
22
|
+
stakeholder_objection: ['stakeholder_objection'],
|
|
23
|
+
execution_risk: ['execution_risk'],
|
|
24
|
+
ai_answer_trust: ['claim_evidence', 'human_judgment'],
|
|
25
|
+
full_judgment_review: [...ALL_LENS_IDS],
|
|
26
|
+
};
|
|
27
|
+
export function routeLenses(profile, artifact, opts = { maxLensCalls: 7 }) {
|
|
28
|
+
const isDeck = artifact.detected_structure.is_deck;
|
|
29
|
+
const concerns = opts.concerns ?? [];
|
|
30
|
+
// Candidate set: base + profile-driven + concern-driven, filtered by applies_to.
|
|
31
|
+
const wanted = new Set(BASE);
|
|
32
|
+
if (isDeck)
|
|
33
|
+
wanted.add('deck_narrative');
|
|
34
|
+
if (STAKES_ORDER[profile.stakes] >= STAKES_ORDER.medium)
|
|
35
|
+
wanted.add('stakeholder_objection');
|
|
36
|
+
if (profile.intent === 'request_approval' || profile.intent === 'persuade' || profile.intent === 'pitch') {
|
|
37
|
+
wanted.add('stakeholder_objection');
|
|
38
|
+
}
|
|
39
|
+
if (['prd', 'proposal', 'strategy_memo'].includes(profile.document_type))
|
|
40
|
+
wanted.add('execution_risk');
|
|
41
|
+
if (['rfc', 'adr', 'strategy_memo', 'proposal'].includes(profile.document_type))
|
|
42
|
+
wanted.add('reversibility');
|
|
43
|
+
const concernRequested = new Set();
|
|
44
|
+
for (const c of concerns)
|
|
45
|
+
for (const id of CONCERN_TO_LENS[c] ?? []) {
|
|
46
|
+
wanted.add(id);
|
|
47
|
+
concernRequested.add(id);
|
|
48
|
+
}
|
|
49
|
+
// Keep lenses that pass structural gates (deck/document-type — never bypassed).
|
|
50
|
+
// Soft gates (stakes/intent/audience/maturity) can be bypassed when the user
|
|
51
|
+
// explicitly asked for the lens via a concern chip.
|
|
52
|
+
const matched = [...wanted].filter((id) => appliesStructural(LENSES[id], profile, isDeck) && (concernRequested.has(id) || appliesSoft(LENSES[id], profile)));
|
|
53
|
+
// Priority: base spine first, then deck/objection/execution, cap at budget.
|
|
54
|
+
const priority = orderByPriority(matched, concerns);
|
|
55
|
+
const selected = priority.slice(0, Math.max(1, opts.maxLensCalls));
|
|
56
|
+
const cutForBudget = priority.slice(selected.length);
|
|
57
|
+
const skipped = [];
|
|
58
|
+
for (const id of ALL_LENS_IDS) {
|
|
59
|
+
if (selected.includes(id))
|
|
60
|
+
continue;
|
|
61
|
+
if (cutForBudget.includes(id)) {
|
|
62
|
+
skipped.push({ id, reason: '분석 예산(렌즈 수) 초과로 이번엔 제외' });
|
|
63
|
+
}
|
|
64
|
+
else if (!applies(LENSES[id], profile, isDeck)) {
|
|
65
|
+
skipped.push({ id, reason: skipReason(id, profile, isDeck) });
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
skipped.push({ id, reason: '이 문서에는 우선순위가 낮아 제외' });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { selected, skipped, disclosure: buildDisclosure(selected, profile) };
|
|
72
|
+
}
|
|
73
|
+
/** Structural gates — a lens simply cannot apply to this document shape. */
|
|
74
|
+
export function appliesStructural(lens, profile, isDeck) {
|
|
75
|
+
const f = lens.applies_to;
|
|
76
|
+
if (f.deck_only && !isDeck)
|
|
77
|
+
return false;
|
|
78
|
+
if (f.document_type && !f.document_type.includes(profile.document_type))
|
|
79
|
+
return false;
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
/** Soft gates — relevance heuristics a user concern may override. */
|
|
83
|
+
export function appliesSoft(lens, profile) {
|
|
84
|
+
const f = lens.applies_to;
|
|
85
|
+
if (f.artifact_maturity && !f.artifact_maturity.includes(profile.artifact_maturity))
|
|
86
|
+
return false;
|
|
87
|
+
if (f.intent && !f.intent.includes(profile.intent))
|
|
88
|
+
return false;
|
|
89
|
+
if (f.audience && !f.audience.includes(profile.audience))
|
|
90
|
+
return false;
|
|
91
|
+
if (f.min_stakes && STAKES_ORDER[profile.stakes] < STAKES_ORDER[f.min_stakes])
|
|
92
|
+
return false;
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
/** Full applies check (structural + soft), used by callers that don't route. */
|
|
96
|
+
export function applies(lens, profile, isDeck) {
|
|
97
|
+
return appliesStructural(lens, profile, isDeck) && appliesSoft(lens, profile);
|
|
98
|
+
}
|
|
99
|
+
function orderByPriority(ids, concerns) {
|
|
100
|
+
const concernBoost = new Set();
|
|
101
|
+
for (const c of concerns)
|
|
102
|
+
for (const id of CONCERN_TO_LENS[c] ?? [])
|
|
103
|
+
concernBoost.add(id);
|
|
104
|
+
const rank = (id) => {
|
|
105
|
+
let r = BASE.includes(id) ? 0 : 5;
|
|
106
|
+
if (id === 'deck_narrative')
|
|
107
|
+
r = 2; // decks: narrative is load-bearing
|
|
108
|
+
if (concernBoost.has(id))
|
|
109
|
+
r -= 3; // user asked for it
|
|
110
|
+
return r;
|
|
111
|
+
};
|
|
112
|
+
return [...ids].sort((a, b) => rank(a) - rank(b) || ALL_LENS_IDS.indexOf(a) - ALL_LENS_IDS.indexOf(b));
|
|
113
|
+
}
|
|
114
|
+
function skipReason(id, profile, isDeck) {
|
|
115
|
+
if (id === 'deck_narrative' && !isDeck)
|
|
116
|
+
return '덱이 아니어서 제외';
|
|
117
|
+
if (id === 'reversibility')
|
|
118
|
+
return '되돌림 판단이 핵심이 아닌 문서 유형이어서 제외';
|
|
119
|
+
if (id === 'stakeholder_objection')
|
|
120
|
+
return '이해관계/stakes가 낮아 제외';
|
|
121
|
+
void profile;
|
|
122
|
+
return '이 문서에 해당하지 않아 제외';
|
|
123
|
+
}
|
|
124
|
+
function buildDisclosure(selected, profile) {
|
|
125
|
+
const labels = selected.map((id) => LENSES[id].label).join(', ');
|
|
126
|
+
const typeKo = documentTypeKo(profile.document_type);
|
|
127
|
+
return `적용한 검수 렌즈: ${labels}. 이유: 이 문서는 ${typeKo}이고, 핵심 주장과 사람이 책임질 판단을 우선 확인했습니다.`;
|
|
128
|
+
}
|
|
129
|
+
function documentTypeKo(t) {
|
|
130
|
+
const map = {
|
|
131
|
+
strategy_memo: '전략 메모',
|
|
132
|
+
prd: '제품 요구사항 문서(PRD)',
|
|
133
|
+
rfc: 'RFC',
|
|
134
|
+
adr: 'ADR',
|
|
135
|
+
strategy_deck: '전략 덱',
|
|
136
|
+
pitch_deck: '피치 덱',
|
|
137
|
+
board_deck: '보드 덱',
|
|
138
|
+
sales_deck: '세일즈 덱',
|
|
139
|
+
investor_update: '투자자 업데이트',
|
|
140
|
+
research_report: '리서치 리포트',
|
|
141
|
+
meeting_notes: '회의록',
|
|
142
|
+
llm_answer: 'AI 답변',
|
|
143
|
+
proposal: '제안서',
|
|
144
|
+
unknown: '문서',
|
|
145
|
+
};
|
|
146
|
+
return map[t] ?? '문서';
|
|
147
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argus Document Judgment Review — Phase 0 Schema Lock.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the intermediate objects that the review
|
|
5
|
+
* pipeline produces. Per the design doc
|
|
6
|
+
* (docs/strategy/argus-product-loop-ux-subtask-b-2026-07-01.md → "구현 순서 /
|
|
7
|
+
* Phase 0: Schema Lock"): fix the objects BEFORE the pipeline and the UI, so
|
|
8
|
+
* the same `JudgmentReceipt` can be produced by the webapp and the MCP/plugin.
|
|
9
|
+
*
|
|
10
|
+
* Design invariants encoded here (do not weaken without revisiting the doc):
|
|
11
|
+
* - Every high-confidence finding MUST be able to carry a `source_anchor`.
|
|
12
|
+
* A finding with no anchor cannot be `high` confidence (enforced in the
|
|
13
|
+
* pipeline, typed as optional here so degraded reviews can still exist).
|
|
14
|
+
* - Provenance (schema/lens/model versions) rides on every receipt, so we can
|
|
15
|
+
* later prove "Argus got better" instead of asserting it.
|
|
16
|
+
* - Ownership fields (lean, pass/fail, settlement outcome) are user-owned.
|
|
17
|
+
* The pipeline never fills them — see the Zero-Judgment spine in CLAUDE.md.
|
|
18
|
+
*/
|
|
19
|
+
export const REVIEW_SCHEMA_VERSION = '1';
|
|
20
|
+
export const DEFAULT_BUDGET = {
|
|
21
|
+
quick: { max_units: 60, max_tokens: 8000, max_lens_calls: 4, depth: 'quick' },
|
|
22
|
+
standard: { max_units: 160, max_tokens: 16000, max_lens_calls: 7, depth: 'standard' },
|
|
23
|
+
deep: { max_units: 400, max_tokens: 32000, max_lens_calls: 9, depth: 'deep' },
|
|
24
|
+
};
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Reviewability banding helper (pure — safe to import anywhere).
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
export function reviewabilityBand(score) {
|
|
29
|
+
if (score >= 80)
|
|
30
|
+
return 'normal';
|
|
31
|
+
if (score >= 60)
|
|
32
|
+
return 'caveated';
|
|
33
|
+
if (score >= 40)
|
|
34
|
+
return 'limited';
|
|
35
|
+
return 'insufficient';
|
|
36
|
+
}
|