@tienne/gestalt 0.99.0 → 0.100.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/CLAUDE.md +1 -1
- package/dist/package.json +2 -2
- package/dist/plugin/research-sources/amplitude/SOURCE.md +28 -0
- package/dist/plugin/research-sources/app-store-reviews/SOURCE.md +25 -0
- package/dist/plugin/research-sources/app-store-search/SOURCE.md +25 -0
- package/dist/plugin/research-sources/tech-blog-search/SOURCE.md +27 -0
- package/dist/plugin/research-sources/uibowl/SOURCE.md +26 -0
- package/dist/plugin/research-sources/web-search/SOURCE.md +24 -0
- package/dist/plugin/skills/_shared/proactive-routing.md +5 -0
- package/dist/plugin/skills/market-scan/SKILL.md +61 -0
- package/dist/plugin/skills/research/SKILL.md +438 -0
- package/dist/plugin/skills/research/references/design.md +938 -0
- package/dist/plugin/skills/research/references/output-template.md +366 -0
- package/dist/plugin/skills/research/scripts/pii-vision.swift +327 -0
- package/dist/plugin/skills/research/scripts/slide-extract.mjs +223 -0
- package/dist/plugin/skills/reverse-plan/SKILL.md +52 -0
- package/dist/plugin/skills/screen-ref/SKILL.md +63 -0
- package/dist/plugin/skills/ux-research/SKILL.md +51 -0
- package/dist/src/core/config.d.ts +3 -0
- package/dist/src/core/config.d.ts.map +1 -1
- package/dist/src/core/config.js +4 -0
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/mcp/schemas.d.ts +93 -0
- package/dist/src/mcp/schemas.d.ts.map +1 -1
- package/dist/src/mcp/schemas.js +110 -0
- package/dist/src/mcp/schemas.js.map +1 -1
- package/dist/src/mcp/server.d.ts.map +1 -1
- package/dist/src/mcp/server.js +11 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/mcp/tools/research-passthrough.d.ts +21 -0
- package/dist/src/mcp/tools/research-passthrough.d.ts.map +1 -0
- package/dist/src/mcp/tools/research-passthrough.js +566 -0
- package/dist/src/mcp/tools/research-passthrough.js.map +1 -0
- package/dist/src/research/canvas-view.d.ts +5 -0
- package/dist/src/research/canvas-view.d.ts.map +1 -0
- package/dist/src/research/canvas-view.js +274 -0
- package/dist/src/research/canvas-view.js.map +1 -0
- package/dist/src/research/evidence.d.ts +145 -0
- package/dist/src/research/evidence.d.ts.map +1 -0
- package/dist/src/research/evidence.js +50 -0
- package/dist/src/research/evidence.js.map +1 -0
- package/dist/src/research/figma-plan.d.ts +80 -0
- package/dist/src/research/figma-plan.d.ts.map +1 -0
- package/dist/src/research/figma-plan.js +527 -0
- package/dist/src/research/figma-plan.js.map +1 -0
- package/dist/src/research/figma-script.d.ts +19 -0
- package/dist/src/research/figma-script.d.ts.map +1 -0
- package/dist/src/research/figma-script.js +363 -0
- package/dist/src/research/figma-script.js.map +1 -0
- package/dist/src/research/index.d.ts +8 -0
- package/dist/src/research/index.d.ts.map +1 -0
- package/dist/src/research/index.js +8 -0
- package/dist/src/research/index.js.map +1 -0
- package/dist/src/research/pii-core.d.ts +16 -0
- package/dist/src/research/pii-core.d.ts.map +1 -0
- package/dist/src/research/pii-core.js +345 -0
- package/dist/src/research/pii-core.js.map +1 -0
- package/dist/src/research/pii-dom.d.ts +13 -0
- package/dist/src/research/pii-dom.d.ts.map +1 -0
- package/dist/src/research/pii-dom.js +208 -0
- package/dist/src/research/pii-dom.js.map +1 -0
- package/dist/src/research/pii-image.d.ts +71 -0
- package/dist/src/research/pii-image.d.ts.map +1 -0
- package/dist/src/research/pii-image.js +163 -0
- package/dist/src/research/pii-image.js.map +1 -0
- package/dist/src/research/pii.d.ts +21 -0
- package/dist/src/research/pii.d.ts.map +1 -0
- package/dist/src/research/pii.js +47 -0
- package/dist/src/research/pii.js.map +1 -0
- package/dist/src/research/product-profile.d.ts +108 -0
- package/dist/src/research/product-profile.d.ts.map +1 -0
- package/dist/src/research/product-profile.js +104 -0
- package/dist/src/research/product-profile.js.map +1 -0
- package/dist/src/research/render-html.d.ts +203 -0
- package/dist/src/research/render-html.d.ts.map +1 -0
- package/dist/src/research/render-html.js +901 -0
- package/dist/src/research/render-html.js.map +1 -0
- package/dist/src/research/schema-base.d.ts +5 -0
- package/dist/src/research/schema-base.d.ts.map +1 -0
- package/dist/src/research/schema-base.js +10 -0
- package/dist/src/research/schema-base.js.map +1 -0
- package/dist/src/research/schema.d.ts +5142 -0
- package/dist/src/research/schema.d.ts.map +1 -0
- package/dist/src/research/schema.js +991 -0
- package/dist/src/research/schema.js.map +1 -0
- package/dist/src/research/source-create.d.ts +79 -0
- package/dist/src/research/source-create.d.ts.map +1 -0
- package/dist/src/research/source-create.js +226 -0
- package/dist/src/research/source-create.js.map +1 -0
- package/dist/src/research/source-registry.d.ts +29 -0
- package/dist/src/research/source-registry.d.ts.map +1 -0
- package/dist/src/research/source-registry.js +74 -0
- package/dist/src/research/source-registry.js.map +1 -0
- package/dist/src/research/source-schema.d.ts +158 -0
- package/dist/src/research/source-schema.d.ts.map +1 -0
- package/dist/src/research/source-schema.js +215 -0
- package/dist/src/research/source-schema.js.map +1 -0
- package/dist/src/research/source-status.d.ts +40 -0
- package/dist/src/research/source-status.d.ts.map +1 -0
- package/dist/src/research/source-status.js +109 -0
- package/dist/src/research/source-status.js.map +1 -0
- package/dist/src/research/store.d.ts +24 -0
- package/dist/src/research/store.d.ts.map +1 -0
- package/dist/src/research/store.js +60 -0
- package/dist/src/research/store.js.map +1 -0
- package/dist/src/research/target-view.d.ts +33 -0
- package/dist/src/research/target-view.d.ts.map +1 -0
- package/dist/src/research/target-view.js +133 -0
- package/dist/src/research/target-view.js.map +1 -0
- package/dist/src/research/target.d.ts +883 -0
- package/dist/src/research/target.d.ts.map +1 -0
- package/dist/src/research/target.js +323 -0
- package/dist/src/research/target.js.map +1 -0
- package/dist/src/research/video-transfer.d.ts +28 -0
- package/dist/src/research/video-transfer.d.ts.map +1 -0
- package/dist/src/research/video-transfer.js +100 -0
- package/dist/src/research/video-transfer.js.map +1 -0
- package/package.json +2 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -1
- package/plugin/mcp.json +1 -1
- package/plugin/research-sources/amplitude/SOURCE.md +28 -0
- package/plugin/research-sources/app-store-reviews/SOURCE.md +25 -0
- package/plugin/research-sources/app-store-search/SOURCE.md +25 -0
- package/plugin/research-sources/tech-blog-search/SOURCE.md +27 -0
- package/plugin/research-sources/uibowl/SOURCE.md +26 -0
- package/plugin/research-sources/web-search/SOURCE.md +24 -0
- package/plugin/skills/_shared/proactive-routing.md +5 -0
- package/plugin/skills/market-scan/SKILL.md +61 -0
- package/plugin/skills/research/SKILL.md +438 -0
- package/plugin/skills/research/references/design.md +938 -0
- package/plugin/skills/research/references/output-template.md +366 -0
- package/plugin/skills/research/scripts/pii-vision.swift +327 -0
- package/plugin/skills/research/scripts/slide-extract.mjs +223 -0
- package/plugin/skills/reverse-plan/SKILL.md +52 -0
- package/plugin/skills/screen-ref/SKILL.md +63 -0
- package/plugin/skills/ux-research/SKILL.md +51 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"target.d.ts","sourceRoot":"","sources":["../../../src/research/target.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { basisFields, checkBasis } from './evidence.js';
|
|
3
|
+
import { httpsUrlSchema, idSchema, textSchema } from './schema-base.js';
|
|
4
|
+
/** improve: 이미 있는 화면을 고친다. new: 아직 없는 화면이라 기준값을 비슷한 퍼널에서 빌려온다 */
|
|
5
|
+
export const TARGET_MODES = ['improve', 'new'];
|
|
6
|
+
/** 목표치 계산 한 단계가 어디서 온 숫자인지. 위에 있을수록 믿을 만하다 */
|
|
7
|
+
export const TARGET_STEP_METHODS = [
|
|
8
|
+
'internal',
|
|
9
|
+
'proxy',
|
|
10
|
+
'business',
|
|
11
|
+
'market',
|
|
12
|
+
'case',
|
|
13
|
+
'assumption',
|
|
14
|
+
];
|
|
15
|
+
export const TRACKING_STATUSES = ['exists', 'missing', 'planned'];
|
|
16
|
+
export const LIFT_METHOD_LABEL = {
|
|
17
|
+
experiment: '우리 지난 실험',
|
|
18
|
+
case: '타사 사례',
|
|
19
|
+
market: '시장 자료',
|
|
20
|
+
assumption: '가정',
|
|
21
|
+
};
|
|
22
|
+
export const SHORTFALL_DECISION_LABEL = {
|
|
23
|
+
'add-hypotheses': '가설 추가',
|
|
24
|
+
'swap-hypotheses': '효과 큰 가설로 교체',
|
|
25
|
+
'lower-target': '목표 낮춤',
|
|
26
|
+
keep: '그대로 둠',
|
|
27
|
+
};
|
|
28
|
+
export const DATA_SOURCE_KINDS = ['amplitude', 'export'];
|
|
29
|
+
/** 가설 기대 효과가 어디서 온 값인지. 위에 있을수록 믿을 만하다 */
|
|
30
|
+
export const LIFT_METHODS = ['experiment', 'case', 'market', 'assumption'];
|
|
31
|
+
/** 가설을 다 성공해도 목표에 못 닿을 때 사용자와 고르는 대응 */
|
|
32
|
+
export const SHORTFALL_DECISIONS = [
|
|
33
|
+
'add-hypotheses',
|
|
34
|
+
'swap-hypotheses',
|
|
35
|
+
'lower-target',
|
|
36
|
+
'keep',
|
|
37
|
+
];
|
|
38
|
+
/** 분석 도구 키처럼 보이는 값. 프로젝트 이름이나 파일 경로 자리에 키를 붙여 넣는 실수를 막는다 */
|
|
39
|
+
const KEY_LIKE = /\b[0-9a-f]{32}\b/i;
|
|
40
|
+
export const looksLikeKey = (s) => KEY_LIKE.test(s);
|
|
41
|
+
const noKey = (s) => !looksLikeKey(s);
|
|
42
|
+
export const KEY_MESSAGE = '키처럼 보이는 값은 적지 않는다. 키가 있는 파일 경로만 적는다';
|
|
43
|
+
const pctSchema = z.number().min(0).max(100);
|
|
44
|
+
const isUsableBasis = (basis) => basis !== 'unconfirmed' && basis !== 'absent';
|
|
45
|
+
const baselineSchema = z
|
|
46
|
+
.object({
|
|
47
|
+
/** 지금 값(%). 못 구했으면 비우고 basis를 unconfirmed로 둔다 */
|
|
48
|
+
valuePct: pctSchema.optional(),
|
|
49
|
+
/** 어느 기간의 값인지. 예: "2026-08-01부터 2026-09-25까지 8주" */
|
|
50
|
+
period: textSchema,
|
|
51
|
+
/** 신규 화면이라 대신 쓴 기존 퍼널. 예: "포인트 충전 진입 대비 결제 완료" */
|
|
52
|
+
proxyOf: z.string().optional(),
|
|
53
|
+
...basisFields,
|
|
54
|
+
})
|
|
55
|
+
.superRefine((b, ctx) => {
|
|
56
|
+
checkBasis(b, ctx);
|
|
57
|
+
if (b.valuePct === undefined && isUsableBasis(b.basis)) {
|
|
58
|
+
ctx.addIssue({
|
|
59
|
+
code: z.ZodIssueCode.custom,
|
|
60
|
+
path: ['valuePct'],
|
|
61
|
+
message: '값이 없으면 basis를 unconfirmed로 두고 이유를 적는다',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const entrantsSchema = z
|
|
66
|
+
.object({
|
|
67
|
+
/** 분모에 들어오는 하루 사람 수 */
|
|
68
|
+
value: z.number().positive(),
|
|
69
|
+
/** 신규 화면이면 어떻게 추정했는지. 예: "마이 탭 하루 조회 6만 × 비슷한 배너 클릭률 7%" */
|
|
70
|
+
derivation: z.string().optional(),
|
|
71
|
+
...basisFields,
|
|
72
|
+
})
|
|
73
|
+
.superRefine(checkBasis);
|
|
74
|
+
const rangeSchema = z
|
|
75
|
+
.object({
|
|
76
|
+
conservativePct: pctSchema,
|
|
77
|
+
expectedPct: pctSchema,
|
|
78
|
+
stretchPct: pctSchema,
|
|
79
|
+
})
|
|
80
|
+
.refine((r) => r.conservativePct <= r.expectedPct && r.expectedPct <= r.stretchPct, '목표 범위는 보수 ≤ 기대 ≤ 도전 순서다');
|
|
81
|
+
const targetStepSchema = z
|
|
82
|
+
.object({
|
|
83
|
+
id: idSchema,
|
|
84
|
+
label: textSchema,
|
|
85
|
+
/** 값과 단위를 같이. 예: "하루 4,200명" */
|
|
86
|
+
value: textSchema,
|
|
87
|
+
method: z.enum(TARGET_STEP_METHODS),
|
|
88
|
+
...basisFields,
|
|
89
|
+
})
|
|
90
|
+
.superRefine(checkBasis);
|
|
91
|
+
const dataSourceSchema = z.object({
|
|
92
|
+
kind: z.enum(DATA_SOURCE_KINDS),
|
|
93
|
+
/** 프로젝트 이름. 키는 적지 않는다 */
|
|
94
|
+
project: textSchema.refine(noKey, KEY_MESSAGE),
|
|
95
|
+
/** 같은 숫자를 다시 볼 수 있는 저장된 차트 */
|
|
96
|
+
chartUrl: httpsUrlSchema.optional(),
|
|
97
|
+
/** 퍼널 단계마다 쓴 이벤트 */
|
|
98
|
+
events: z.array(z.object({ step: textSchema, event: textSchema })).default([]),
|
|
99
|
+
from: textSchema,
|
|
100
|
+
to: textSchema,
|
|
101
|
+
});
|
|
102
|
+
const trackingItemSchema = z.object({
|
|
103
|
+
event: textSchema,
|
|
104
|
+
/** 퍼널의 어느 단계를 재는지 */
|
|
105
|
+
step: textSchema,
|
|
106
|
+
status: z.enum(TRACKING_STATUSES),
|
|
107
|
+
note: z.string().optional(),
|
|
108
|
+
});
|
|
109
|
+
/** 가설의 기대 효과(expectedLiftPp)가 어디서 왔는지. 표본과 목표 도달 계산이 전부 이 값에서 나온다 */
|
|
110
|
+
export const liftBasisSchema = z
|
|
111
|
+
.object({
|
|
112
|
+
method: z.enum(LIFT_METHODS),
|
|
113
|
+
...basisFields,
|
|
114
|
+
})
|
|
115
|
+
.superRefine(checkBasis);
|
|
116
|
+
const shortfallSchema = z
|
|
117
|
+
.object({
|
|
118
|
+
decision: z.enum(SHORTFALL_DECISIONS),
|
|
119
|
+
/** 목표를 낮췄으면 처음 기대 목표(%) */
|
|
120
|
+
originalExpectedPct: pctSchema.optional(),
|
|
121
|
+
/** 누가 왜 그렇게 정했는지 */
|
|
122
|
+
note: textSchema,
|
|
123
|
+
})
|
|
124
|
+
.refine((s) => s.decision !== 'lower-target' || s.originalExpectedPct !== undefined, {
|
|
125
|
+
message: '목표를 낮췄으면 처음 기대 목표(originalExpectedPct)를 적는다',
|
|
126
|
+
path: ['originalExpectedPct'],
|
|
127
|
+
});
|
|
128
|
+
export const targetSchema = z.object({
|
|
129
|
+
mode: z.enum(TARGET_MODES),
|
|
130
|
+
metric: z.object({
|
|
131
|
+
name: textSchema,
|
|
132
|
+
numerator: textSchema,
|
|
133
|
+
denominator: textSchema,
|
|
134
|
+
}),
|
|
135
|
+
baseline: baselineSchema,
|
|
136
|
+
dailyEntrants: entrantsSchema.optional(),
|
|
137
|
+
/** 주마다 출렁이는 폭(%p). 이보다 작은 효과는 실험 없이는 안 보인다 */
|
|
138
|
+
weeklySpreadPp: z.number().nonnegative().optional(),
|
|
139
|
+
/** 아무것도 안 해도 목표 시점까지 움직일 몫(%p). 내려가면 음수 */
|
|
140
|
+
trendPp: z.number().optional(),
|
|
141
|
+
range: rangeSchema.optional(),
|
|
142
|
+
steps: z.array(targetStepSchema).default([]),
|
|
143
|
+
dataSource: dataSourceSchema.optional(),
|
|
144
|
+
/** 가설을 재는 데 필요한 이벤트. 신규 화면이면 심어야 할 목록이 된다 */
|
|
145
|
+
trackingPlan: z.array(trackingItemSchema).default([]),
|
|
146
|
+
/** 가설을 다 성공해도 기대 목표에 못 닿았을 때 사용자와 고른 대응 */
|
|
147
|
+
shortfall: shortfallSchema.optional(),
|
|
148
|
+
});
|
|
149
|
+
// 두 비율 비교, 양측 유의수준 0.05, 검정력 0.8
|
|
150
|
+
const Z_ALPHA = 1.959964;
|
|
151
|
+
const Z_BETA = 0.841621;
|
|
152
|
+
/** 이보다 오래 걸리는 실험은 이 트래픽으로 못 가려낸다고 본다 */
|
|
153
|
+
export const MAX_TEST_DAYS = 56;
|
|
154
|
+
/** 기준 전환율 basePct에서 liftPp만큼 오른 걸 가려내는 데 필요한 군당 표본 */
|
|
155
|
+
export function requiredSamplePerArm(basePct, liftPp) {
|
|
156
|
+
const p1 = basePct / 100;
|
|
157
|
+
const p2 = (basePct + liftPp) / 100;
|
|
158
|
+
if (liftPp <= 0 || p2 >= 1)
|
|
159
|
+
return Number.POSITIVE_INFINITY;
|
|
160
|
+
const pBar = (p1 + p2) / 2;
|
|
161
|
+
const a = Z_ALPHA * Math.sqrt(2 * pBar * (1 - pBar));
|
|
162
|
+
const b = Z_BETA * Math.sqrt(p1 * (1 - p1) + p2 * (1 - p2));
|
|
163
|
+
return Math.ceil((a + b) ** 2 / (p2 - p1) ** 2);
|
|
164
|
+
}
|
|
165
|
+
/** 군당 표본 n으로 가려낼 수 있는 가장 작은 효과(%p). 소수 첫째 자리까지 */
|
|
166
|
+
export function minDetectableLiftPp(basePct, nPerArm) {
|
|
167
|
+
let lo = 0;
|
|
168
|
+
let hi = 100 - basePct;
|
|
169
|
+
if (requiredSamplePerArm(basePct, hi - 1e-9) > nPerArm)
|
|
170
|
+
return Number.POSITIVE_INFINITY;
|
|
171
|
+
for (let i = 0; i < 40; i++) {
|
|
172
|
+
const mid = (lo + hi) / 2;
|
|
173
|
+
if (requiredSamplePerArm(basePct, mid) <= nPerArm)
|
|
174
|
+
hi = mid;
|
|
175
|
+
else
|
|
176
|
+
lo = mid;
|
|
177
|
+
}
|
|
178
|
+
return Math.ceil(hi * 10) / 10;
|
|
179
|
+
}
|
|
180
|
+
export function daysNeeded(nPerArm, dailyEntrants, arms = 2) {
|
|
181
|
+
return Math.ceil((nPerArm * arms) / dailyEntrants);
|
|
182
|
+
}
|
|
183
|
+
/** 기대 효과가 하나도 없거나 기준값, 목표 범위가 없으면 계산하지 않는다 */
|
|
184
|
+
export function liftCoverage(research) {
|
|
185
|
+
const t = research.target;
|
|
186
|
+
const base = t?.baseline.valuePct;
|
|
187
|
+
if (!t?.range || base === undefined)
|
|
188
|
+
return undefined;
|
|
189
|
+
const lifts = research.verification.flatMap((v) => v.expectedLiftPp === undefined ? [] : [v.expectedLiftPp]);
|
|
190
|
+
if (lifts.length === 0)
|
|
191
|
+
return undefined;
|
|
192
|
+
const trend = t.trendPp ?? 0;
|
|
193
|
+
const plannedPp = round1(lifts.reduce((s, x) => s + x, 0));
|
|
194
|
+
return {
|
|
195
|
+
neededPp: round1(t.range.expectedPct - base - trend),
|
|
196
|
+
plannedPp,
|
|
197
|
+
reachPct: round1(base + trend + plannedPp),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/** 기대 효과가 적힌 가설마다 필요한 표본과 걸리는 일수 */
|
|
201
|
+
export function hypothesisPower(research) {
|
|
202
|
+
const t = research.target;
|
|
203
|
+
const base = t?.baseline.valuePct;
|
|
204
|
+
if (!t || base === undefined)
|
|
205
|
+
return [];
|
|
206
|
+
return research.verification.flatMap((v, index) => {
|
|
207
|
+
if (v.expectedLiftPp === undefined)
|
|
208
|
+
return [];
|
|
209
|
+
const nPerArm = requiredSamplePerArm(base, v.expectedLiftPp);
|
|
210
|
+
const days = t.dailyEntrants && Number.isFinite(nPerArm)
|
|
211
|
+
? daysNeeded(nPerArm, t.dailyEntrants.value)
|
|
212
|
+
: undefined;
|
|
213
|
+
return [{ index, liftPp: v.expectedLiftPp, nPerArm, ...(days === undefined ? {} : { days }) }];
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/** 기준값을 실제 데이터로 채웠는지. 채웠으면 타사 사례는 교차 확인용으로 내려간다 */
|
|
217
|
+
export function hasUsableBaseline(research) {
|
|
218
|
+
const b = research.target?.baseline;
|
|
219
|
+
return !!b && b.valuePct !== undefined && isUsableBasis(b.basis);
|
|
220
|
+
}
|
|
221
|
+
const round1 = (x) => Math.round(x * 10) / 10;
|
|
222
|
+
export function lintTarget(research, warnings) {
|
|
223
|
+
const t = research.target;
|
|
224
|
+
if (!t) {
|
|
225
|
+
if (research.verification.length > 0) {
|
|
226
|
+
warnings.push({
|
|
227
|
+
path: 'target',
|
|
228
|
+
message: '목표치 근거가 없다. 기준값과 하루 진입 수를 기존 데이터에서 뽑아 목표 범위를 계산한다',
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (!hasUsableBaseline(research)) {
|
|
234
|
+
warnings.push({
|
|
235
|
+
path: 'target.baseline',
|
|
236
|
+
message: '기준값을 아직 못 구했다. 열린 질문(owner: data)으로 넘긴다',
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (t.mode === 'new' && !t.baseline.proxyOf?.trim()) {
|
|
240
|
+
warnings.push({
|
|
241
|
+
path: 'target.baseline.proxyOf',
|
|
242
|
+
message: '신규 화면인데 기준값을 어느 기존 퍼널에서 빌렸는지 없다',
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
if (!t.range) {
|
|
246
|
+
warnings.push({ path: 'target.range', message: '목표 범위(보수, 기대, 도전)가 없다' });
|
|
247
|
+
}
|
|
248
|
+
const coverage = liftCoverage(research);
|
|
249
|
+
if (coverage && coverage.plannedPp < coverage.neededPp) {
|
|
250
|
+
const gap = `기대 효과 합 ${coverage.plannedPp}%p, 필요 ${coverage.neededPp}%p`;
|
|
251
|
+
const decision = t.shortfall?.decision;
|
|
252
|
+
if (!decision) {
|
|
253
|
+
warnings.push({
|
|
254
|
+
path: 'verification',
|
|
255
|
+
message: `가설을 다 성공해도 목표에 못 닿는다 (${gap}). 가설 추가, 효과 큰 가설로 교체, 목표 낮춤, 그대로 둠 중에서 사용자와 골라 target.shortfall에 적는다`,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
else if (decision !== 'keep') {
|
|
259
|
+
warnings.push({
|
|
260
|
+
path: 'target.shortfall',
|
|
261
|
+
message: `목표에 못 닿아 고른 대응은 '${SHORTFALL_DECISION_LABEL[decision]}'인데 아직 못 닿는다 (${gap})`,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (!coverage && t.range && research.verification.length > 0) {
|
|
266
|
+
if (!research.verification.some((v) => v.expectedLiftPp !== undefined)) {
|
|
267
|
+
warnings.push({
|
|
268
|
+
path: 'verification',
|
|
269
|
+
message: '기대 효과(expectedLiftPp)가 적힌 가설이 없어서 목표까지 닿는지 못 본다',
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
research.verification.forEach((v, i) => {
|
|
274
|
+
const lift = v.expectedLiftPp;
|
|
275
|
+
if (lift !== undefined && !v.liftBasis) {
|
|
276
|
+
warnings.push({
|
|
277
|
+
path: `verification.${i}.liftBasis`,
|
|
278
|
+
message: `기대 효과 ${lift}%p의 근거가 없다 (가설 ${i + 1}). 우리 지난 실험이나 타사 사례를 찾아 liftBasis에 단다`,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
else if (lift !== undefined &&
|
|
282
|
+
v.liftBasis &&
|
|
283
|
+
(v.liftBasis.method === 'assumption' || !isUsableBasis(v.liftBasis.basis))) {
|
|
284
|
+
warnings.push({
|
|
285
|
+
path: `verification.${i}.liftBasis`,
|
|
286
|
+
message: `기대 효과 ${lift}%p가 가정이다 (가설 ${i + 1}). 이 값으로 계산한 표본과 목표 도달이 같이 흔들린다`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
if (lift !== undefined && t.weeklySpreadPp !== undefined && lift < t.weeklySpreadPp) {
|
|
290
|
+
warnings.push({
|
|
291
|
+
path: `verification.${i}.expectedLiftPp`,
|
|
292
|
+
message: `기대 효과 ${lift}%p가 주별 변동 ${t.weeklySpreadPp}%p보다 작다 (가설 ${i + 1}). 실험 없이 전후 비교로는 안 보인다`,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
for (const p of hypothesisPower(research)) {
|
|
297
|
+
if (p.days !== undefined && p.days > MAX_TEST_DAYS) {
|
|
298
|
+
warnings.push({
|
|
299
|
+
path: `verification.${p.index}.expectedLiftPp`,
|
|
300
|
+
message: `이 트래픽으로는 못 가려낸다 (가설 ${p.index + 1}: 군당 ${p.nPerArm.toLocaleString('ko-KR')}명, ${p.days}일)`,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const tracked = new Map(t.trackingPlan.map((x) => [x.event, x.status]));
|
|
305
|
+
research.verification.forEach((v, i) => {
|
|
306
|
+
v.events.forEach((event, k) => {
|
|
307
|
+
const status = tracked.get(event);
|
|
308
|
+
if (status === 'missing') {
|
|
309
|
+
warnings.push({
|
|
310
|
+
path: `verification.${i}.events.${k}`,
|
|
311
|
+
message: `측정할 이벤트가 없다 (가설 ${i + 1}: ${event}). 심을지 정한다`,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
else if (status === undefined && t.trackingPlan.length > 0) {
|
|
315
|
+
warnings.push({
|
|
316
|
+
path: `verification.${i}.events.${k}`,
|
|
317
|
+
message: `이벤트 목록(trackingPlan)에 없는 이벤트다 (가설 ${i + 1}: ${event})`,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
//# sourceMappingURL=target.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"target.js","sourceRoot":"","sources":["../../../src/research/target.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ValidationError } from '../core/errors.js';
|
|
2
|
+
import { type Result } from '../core/result.js';
|
|
3
|
+
import { type Research } from './schema.js';
|
|
4
|
+
export interface SplitOutcome {
|
|
5
|
+
/** removeFromMain이면 영상 블록을 뺀 본문, 아니면 받은 그대로 */
|
|
6
|
+
main: Research;
|
|
7
|
+
video: Research;
|
|
8
|
+
/** 영상 문서 폴더로 옮길 캡처 이미지의 상대 경로 */
|
|
9
|
+
imagePaths: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface MergeOutcome {
|
|
12
|
+
main: Research;
|
|
13
|
+
/** 본문 폴더로 옮길 캡처 이미지의 상대 경로 */
|
|
14
|
+
imagePaths: string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 본문의 영상 블록을 `video-analysis` 문서로 떼어낸다. 발견이 가리키는 제품, 프레임 캡처,
|
|
18
|
+
* 그 캡처의 화면 지도 항목만 따라간다. 본문은 기본으로 그대로 둔다
|
|
19
|
+
*/
|
|
20
|
+
export declare function splitVideo(main: Research, videoSlug: string, opts?: {
|
|
21
|
+
removeFromMain?: boolean;
|
|
22
|
+
}): Result<SplitOutcome, ValidationError>;
|
|
23
|
+
/**
|
|
24
|
+
* 영상 문서의 영상 블록을 본문에 합친다. 같은 id가 같은 내용이면 하나로 보고
|
|
25
|
+
* 내용이 다르면 어느 쪽도 덮어쓰지 않고 멈춘다
|
|
26
|
+
*/
|
|
27
|
+
export declare function mergeVideo(main: Research, videoDoc: Research): Result<MergeOutcome, ValidationError>;
|
|
28
|
+
//# sourceMappingURL=video-transfer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-transfer.d.ts","sourceRoot":"","sources":["../../../src/research/video-transfer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAiB,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG3D,MAAM,WAAW,YAAY;IAC3B,+CAA+C;IAC/C,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,QAAQ,CAAC;IAChB,iCAAiC;IACjC,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,QAAQ,CAAC;IACf,8BAA8B;IAC9B,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAWD;;;GAGG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,QAAQ,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE;IAAE,cAAc,CAAC,EAAE,OAAO,CAAA;CAAO,GACtC,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,CAwCvC;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,QAAQ,GACjB,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,CA6CvC"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ValidationError } from '../core/errors.js';
|
|
2
|
+
import { err, ok } from '../core/result.js';
|
|
3
|
+
import { parseResearch } from './schema.js';
|
|
4
|
+
import { isValidSlug } from './store.js';
|
|
5
|
+
const fail = (message) => err(new ValidationError(message, [message]));
|
|
6
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
7
|
+
function reparse(input) {
|
|
8
|
+
return parseResearch(input);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 본문의 영상 블록을 `video-analysis` 문서로 떼어낸다. 발견이 가리키는 제품, 프레임 캡처,
|
|
12
|
+
* 그 캡처의 화면 지도 항목만 따라간다. 본문은 기본으로 그대로 둔다
|
|
13
|
+
*/
|
|
14
|
+
export function splitVideo(main, videoSlug, opts = {}) {
|
|
15
|
+
if (!isValidSlug(videoSlug))
|
|
16
|
+
return fail(`영상 문서의 slug가 올바르지 않다: ${videoSlug}`);
|
|
17
|
+
if (videoSlug === main.meta.slug)
|
|
18
|
+
return fail('영상 문서의 slug가 본문과 같다');
|
|
19
|
+
if (main.meta.kind === 'video-analysis')
|
|
20
|
+
return fail('이미 영상 분석 문서라 더 뗄 게 없다');
|
|
21
|
+
const block = main.video;
|
|
22
|
+
if (!block || block.clips.length === 0)
|
|
23
|
+
return fail('떼어낼 영상 블록이 없다');
|
|
24
|
+
const copy = structuredClone(main);
|
|
25
|
+
const productIds = new Set(block.clips.map((c) => c.productId));
|
|
26
|
+
const captureIds = new Set(block.findings.map((f) => f.captureId).filter((id) => id !== undefined));
|
|
27
|
+
const captures = copy.captures.filter((c) => captureIds.has(c.id));
|
|
28
|
+
const screenIds = new Set(captures.map((c) => c.screenMapId).filter(Boolean));
|
|
29
|
+
const video = reparse({
|
|
30
|
+
schemaVersion: copy.schemaVersion,
|
|
31
|
+
meta: {
|
|
32
|
+
slug: videoSlug,
|
|
33
|
+
title: `${copy.meta.title} 영상 분석`,
|
|
34
|
+
task: copy.meta.task,
|
|
35
|
+
kind: 'video-analysis',
|
|
36
|
+
},
|
|
37
|
+
plan: copy.plan,
|
|
38
|
+
premise: copy.premise,
|
|
39
|
+
productCandidates: copy.productCandidates.filter((p) => productIds.has(p.id)),
|
|
40
|
+
screenMap: copy.screenMap.filter((s) => screenIds.has(s.id)),
|
|
41
|
+
captures,
|
|
42
|
+
video: copy.video,
|
|
43
|
+
});
|
|
44
|
+
if (!video.ok)
|
|
45
|
+
return video;
|
|
46
|
+
let nextMain = main;
|
|
47
|
+
if (opts.removeFromMain) {
|
|
48
|
+
const { video: _removed, ...rest } = structuredClone(main);
|
|
49
|
+
const parsed = reparse(rest);
|
|
50
|
+
if (!parsed.ok)
|
|
51
|
+
return parsed;
|
|
52
|
+
nextMain = parsed.value;
|
|
53
|
+
}
|
|
54
|
+
return ok({ main: nextMain, video: video.value, imagePaths: captures.map((c) => c.path) });
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 영상 문서의 영상 블록을 본문에 합친다. 같은 id가 같은 내용이면 하나로 보고
|
|
58
|
+
* 내용이 다르면 어느 쪽도 덮어쓰지 않고 멈춘다
|
|
59
|
+
*/
|
|
60
|
+
export function mergeVideo(main, videoDoc) {
|
|
61
|
+
const incoming = videoDoc.video;
|
|
62
|
+
if (!incoming || incoming.clips.length === 0)
|
|
63
|
+
return fail('합칠 영상 블록이 없다');
|
|
64
|
+
if (videoDoc.meta.slug === main.meta.slug)
|
|
65
|
+
return fail('본문과 영상 문서가 같은 문서다');
|
|
66
|
+
const next = structuredClone(main);
|
|
67
|
+
const conflicts = [];
|
|
68
|
+
const mergeById = (label, target, items, key) => {
|
|
69
|
+
for (const item of items) {
|
|
70
|
+
const found = target.find((t) => t.id === item.id);
|
|
71
|
+
if (!found)
|
|
72
|
+
target.push(structuredClone(item));
|
|
73
|
+
else if (!same(key(found), key(item)))
|
|
74
|
+
conflicts.push(`${label} ${item.id}`);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
mergeById('제품', next.productCandidates, videoDoc.productCandidates, (p) => p.name);
|
|
78
|
+
mergeById('화면 지도', next.screenMap, videoDoc.screenMap, (s) => [s.productId, s.screen]);
|
|
79
|
+
mergeById('캡처', next.captures, videoDoc.captures, (c) => [c.productId, c.path]);
|
|
80
|
+
const clips = next.video?.clips ?? [];
|
|
81
|
+
const findings = next.video?.findings ?? [];
|
|
82
|
+
mergeById('영상', clips, incoming.clips, (c) => c);
|
|
83
|
+
for (const f of incoming.findings) {
|
|
84
|
+
if (!findings.some((x) => same(x, f)))
|
|
85
|
+
findings.push(structuredClone(f));
|
|
86
|
+
}
|
|
87
|
+
if (conflicts.length > 0) {
|
|
88
|
+
return fail(`같은 id인데 내용이 다르다: ${conflicts.join(', ')}. 한쪽 id를 바꾼 뒤 다시 합친다`);
|
|
89
|
+
}
|
|
90
|
+
next.video = { clips, findings };
|
|
91
|
+
const parsed = reparse(next);
|
|
92
|
+
if (!parsed.ok)
|
|
93
|
+
return parsed;
|
|
94
|
+
const wanted = new Set(videoDoc.captures.map((c) => c.id));
|
|
95
|
+
return ok({
|
|
96
|
+
main: parsed.value,
|
|
97
|
+
imagePaths: parsed.value.captures.filter((c) => wanted.has(c.id)).map((c) => c.path),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=video-transfer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-transfer.js","sourceRoot":"","sources":["../../../src/research/video-transfer.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tienne/gestalt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.100.0",
|
|
4
4
|
"description": "TypeScript AI Development Harness - Gestalt psychology-driven requirement clarification",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"scripts": {
|
|
30
30
|
"dev": "tsx bin/gestalt.ts",
|
|
31
31
|
"build": "tsc -p tsconfig.build.json",
|
|
32
|
-
"postbuild": "rm -rf dist/plugin && mkdir -p dist/plugin && cp -r plugin/agents plugin/role-agents plugin/review-agents plugin/personas plugin/skills dist/plugin/ && cp -r schemas dist/ && cp package.json dist/ && chmod +x dist/bin/gestalt.js dist/bin/gestalt-hook.js && pnpm run verify:plugin && pnpm run verify:rules && pnpm run verify:rule-overlap && pnpm run verify:output-style && pnpm run verify:routing",
|
|
32
|
+
"postbuild": "rm -rf dist/plugin && mkdir -p dist/plugin && cp -r plugin/agents plugin/role-agents plugin/review-agents plugin/personas plugin/research-sources plugin/skills dist/plugin/ && cp -r schemas dist/ && cp package.json dist/ && chmod +x dist/bin/gestalt.js dist/bin/gestalt-hook.js && pnpm run verify:plugin && pnpm run verify:rules && pnpm run verify:rule-overlap && pnpm run verify:output-style && pnpm run verify:routing",
|
|
33
33
|
"verify:plugin": "tsx scripts/verify-plugin-assets.ts",
|
|
34
34
|
"prepublishOnly": "pnpm build",
|
|
35
35
|
"test": "vitest run",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gestalt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.100.0",
|
|
4
4
|
"description": "Gestalt psychology-driven AI development harness. Transforms scattered requirements into structured, validated specifications through interactive interviews.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tienne"
|
package/plugin/.mcp.json
CHANGED
package/plugin/mcp.json
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: amplitude
|
|
3
|
+
description: 우리 제품의 Amplitude에서 목표 지표의 기준값과 하루 진입 수, 주별 변동을 뽑는다
|
|
4
|
+
stage: [baseline]
|
|
5
|
+
access:
|
|
6
|
+
- mcp: { tools: [get_amplitude_context, query_amplitude_data] }
|
|
7
|
+
evidence: observation
|
|
8
|
+
limits: '프로젝트와 이벤트 이름은 사람마다 다르다. 제품 프로필 없이 이름을 짐작해서 질의하지 않는다. 합계만 가져오고 사용자 단위 데이터는 가져오지 않는다'
|
|
9
|
+
fallback: '담당자에게 퍼널 차트에서 내보낸 숫자(기간, 단계별 사용자 수)를 받는다. 그것도 없으면 기준값을 열린 질문(owner: data)으로 남긴다'
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 찾는 법
|
|
13
|
+
|
|
14
|
+
1. `ges_research action=profile product=<제품>`으로 제품 프로필을 읽는다. 없으면 제품 코드 경로를 받아 SDK 초기화와 이벤트 호출을 훑어서 먼저 저장한다. `stale`이 true면 다시 훑을지 사람에게 묻는다.
|
|
15
|
+
2. 도구는 `sources` 응답의 `mcpServer`로 부른다. 서버 이름은 사람마다 달라서 짐작하지 않는다. `get_amplitude_context`로 접근 가능한 프로젝트 목록을 받는다. 프로필의 이벤트 이름이 이벤트 목록에 있는 프로젝트를 후보로 올리고 사람에게 한 번 확인받는다. 프로필에 `amplitudeProject`가 있으면 그걸 먼저 제안한다.
|
|
16
|
+
3. 저장된 차트를 먼저 찾는다. 목표 지표 이름이나 퍼널 단계 이벤트로 검색해서 분자와 분모가 맞는 퍼널 차트가 있으면 그 차트를 돌린다. 차트 링크는 `target.dataSource.chartUrl`에 둔다.
|
|
17
|
+
4. 맞는 차트가 없으면 프로필의 이벤트로 퍼널을 직접 질의한다. 이벤트 이름은 짐작하지 않고 프로젝트의 이벤트 목록에서 확인한 것만 쓴다.
|
|
18
|
+
5. 기간은 최근 4~8주로 잡고 주 단위로 나눠 본다. 전체 전환율은 `baseline.valuePct`, 주별 최대와 최소의 차이 절반은 `weeklySpreadPp`, 첫 단계 하루 평균 사용자 수는 `dailyEntrants`에 둔다.
|
|
19
|
+
6. 새로 만드는 화면이면 기능이 붙는 흐름(`premise.attachTo.flow`)의 기존 퍼널을 먼저 후보로 두고 사람과 같이 고른다. 그 퍼널의 전환율을 기준값으로 쓰고 `proxyOf`에 퍼널 이름을 적는다. 진입 수는 진입점 화면 조회 수에 비슷한 진입점의 클릭률을 곱하고 식을 `derivation`에 적는다.
|
|
20
|
+
7. 근거는 `evidence: internal`로 걸고 차트 이름이나 질의 조건을 `source`에 적는다.
|
|
21
|
+
8. 가설의 기대 효과 근거(`liftBasis`)를 찾을 때는 지난 실험 결과를 읽기 전용으로 본다. 실험 도구가 있으면 목록과 결과만 읽고 비슷한 화면을 바꿨던 실험의 효과를 `method: experiment`로 적는다. 실험 이름과 기간을 `evidence: internal`의 `source`에 적는다. 맞는 실험이 없으면 다른 근거로 넘어간다.
|
|
22
|
+
|
|
23
|
+
## 멈추는 곳
|
|
24
|
+
|
|
25
|
+
- 프로젝트는 사람이 고른다. 후보가 하나여도 확인받는다.
|
|
26
|
+
- 이벤트나 프로젝트 설정을 바꾸는 도구(이벤트 생성, 속성 수정, 차트 저장, 실험과 플래그 수정)는 부르지 않는다. 읽기만 한다.
|
|
27
|
+
- 사용자 목록, 세션 리플레이, 개별 사용자 속성은 열지 않는다. 합계만 쓴다.
|
|
28
|
+
- API 키나 프로젝트 키 값은 리서치 문서와 프로필에 적지 않는다. 키가 있는 파일 경로만 적는다.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: app-store-reviews
|
|
3
|
+
description: 앱스토어 리뷰 RSS에서 사용자가 같이 쓰는 다른 서비스 이름을 뽑는다
|
|
4
|
+
stage: [product-discovery, evidence]
|
|
5
|
+
axis: [same-problem]
|
|
6
|
+
access:
|
|
7
|
+
- http: { host: itunes.apple.com }
|
|
8
|
+
evidence: observation
|
|
9
|
+
limits: "한 번에 최근 리뷰 50개 정도만 줘서 표본이 작다"
|
|
10
|
+
fallback: "후보 확장은 검색어를 바꿔 앱스토어 검색을 반복하는 것으로 대신한다"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 찾는 법
|
|
14
|
+
|
|
15
|
+
1. 후보 앱의 번들이 아니라 스토어 앱 ID(숫자)를 쓴다. 앱스토어 검색 결과의 `trackId`다.
|
|
16
|
+
2. `https://itunes.apple.com/kr/rss/customerreviews/page=1/id=<앱ID>/sortby=mostrecent/json`을 GET으로 부른다.
|
|
17
|
+
3. 리뷰 본문에서 다른 서비스 이름이 나온 것을 센다. 나온 횟수는 `foundVia.reviewMentions`에 적는다.
|
|
18
|
+
4. 상위 후보의 리뷰에서 새 이름을 뽑고 그 앱의 리뷰를 다시 본다. 새 이름이 안 나오면 멈춘다.
|
|
19
|
+
5. 리뷰 문장을 인용할 때는 작성자 이름과 닉네임을 빼고 문장만 적는다.
|
|
20
|
+
|
|
21
|
+
## 멈추는 곳
|
|
22
|
+
|
|
23
|
+
- 리뷰에 개인정보(전화번호, 주문 번호)가 섞여 있으면 인용하지 않는다.
|
|
24
|
+
- 같은 앱을 반복해서 빠르게 부르지 않는다. 앱마다 한 번이면 된다.
|
|
25
|
+
- 표본이 작아서 이름이 한두 번 나온 것은 후보로 올리지 않고 `foundVia`에 참고로만 둔다.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: app-store-search
|
|
3
|
+
description: 앱스토어 검색 API로 제품 후보와 평점 수(규모 짐작)를 찾는다
|
|
4
|
+
stage: [product-discovery, scale]
|
|
5
|
+
axis: [same-market, same-problem]
|
|
6
|
+
access:
|
|
7
|
+
- http: { host: itunes.apple.com, probe: "https://itunes.apple.com/search?term=gift&country=kr&entity=software&limit=1" }
|
|
8
|
+
evidence: observation
|
|
9
|
+
limits: "큰 앱 안의 기능은 검색 결과에 안 나온다. 평점 수는 규모의 대용치이지 사용자 수가 아니다"
|
|
10
|
+
fallback: "기술 블로그와 웹 검색으로 후보를 찾는다"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 찾는 법
|
|
14
|
+
|
|
15
|
+
1. 사용자가 그 기능을 부르는 일반 말로 검색어를 정한다. 한 가지로 끝내지 않고 비슷한 말로 두세 번 더 돌린다.
|
|
16
|
+
2. `https://itunes.apple.com/search?term=<검색어>&country=kr&entity=software&limit=50`을 GET으로 부른다. 키는 필요 없다.
|
|
17
|
+
3. 결과의 장르(`primaryGenreName`), 평점 수(`userRatingCount`), 번들 ID(`bundleId`), 설명(`description`)을 읽는다.
|
|
18
|
+
4. 장르와 설명에 검색어가 있는지로 먼저 거른다. 남은 것은 같은 시장인지 같은 문제인지 판단해 후보로 올린다. 리워드 앱, 게임, 매장 앱이 섞여 나온다.
|
|
19
|
+
5. 평점 수는 `scale`에 `store-rating-count`로 적는다. 규모를 짐작한 것이라 `(해석)`을 붙인다.
|
|
20
|
+
|
|
21
|
+
## 멈추는 곳
|
|
22
|
+
|
|
23
|
+
- 검색어에 공개 전 기획 내용(내부 기능 이름, 가격, 출시 일정)을 넣지 않는다. 사용자가 부르는 일반 말만 쓴다.
|
|
24
|
+
- 요청이 막히거나 오류가 반복되면 간격을 두고 한 번만 다시 시도한 뒤 건너뛴다.
|
|
25
|
+
- 큰 플랫폼 앱 안의 기능은 이 출처로 안 나온다. 기술 블로그와 화면 지도 단계에서 찾는다.
|