@nomadamas/k-skill 0.9.2 → 0.10.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/bin/k-skill.js +64 -43
- package/package.json +2 -2
- package/skills/biz-health-check/instruction.md +2 -0
- package/skills/company-analysis/instruction.md +68 -0
- package/skills/company-analysis/references/source-contract.md +75 -0
- package/skills/company-analysis/scripts/dart-rows.js +52 -0
- package/skills/company-analysis/scripts/dart-rows.ts +48 -0
- package/skills/company-analysis/scripts/evidence.js +567 -0
- package/skills/company-analysis/scripts/evidence.ts +520 -0
- package/skills/company-analysis/scripts/fetch-public.js +85 -0
- package/skills/company-analysis/scripts/fetch-public.ts +75 -0
- package/skills/company-analysis/skill.json +6 -0
- package/skills/express-bus-booking/scripts/kobus_express_booking.py +46 -18
- package/skills/foresttrip-vacancy/scripts/run_foresttrip_vacancy.py +53 -3
- package/skills/g2b-order-plan-search/instruction.md +17 -0
- package/skills/g2b-sanctioned-supplier/instruction.md +10 -1
- package/skills/g2b-sanctioned-supplier/scripts/g2b_sanctioned_supplier.py +8 -4
- package/skills/g2b-sanctioned-supplier/scripts/test_g2b_sanctioned_supplier.py +19 -0
- package/skills/government-bond-analysis/instruction.md +216 -0
- package/skills/government-bond-analysis/references/methodology.md +63 -0
- package/skills/government-bond-analysis/scripts/cli.mjs +268 -0
- package/skills/government-bond-analysis/scripts/collect.mjs +171 -0
- package/skills/government-bond-analysis/scripts/core.mjs +224 -0
- package/skills/government-bond-analysis/scripts/dual.mjs +178 -0
- package/skills/government-bond-analysis/scripts/harness.mjs +406 -0
- package/skills/government-bond-analysis/scripts/modes.mjs +136 -0
- package/skills/government-bond-analysis/scripts/news.mjs +113 -0
- package/skills/government-bond-analysis/scripts/render.mjs +120 -0
- package/skills/government-bond-analysis/scripts/schedule.mjs +96 -0
- package/skills/government-bond-analysis/scripts/ust.mjs +204 -0
- package/skills/government-bond-analysis/skill.json +8 -0
- package/skills/korean-bond-search/instruction.md +122 -0
- package/skills/korean-bond-search/references/fields.md +48 -0
- package/skills/korean-bond-search/references/source-notes.md +11 -0
- package/skills/korean-bond-search/scripts/bond_search.py +906 -0
- package/skills/korean-bond-search/skill.json +9 -0
- package/skills/market-event-impact/instruction.md +150 -0
- package/skills/market-event-impact/references/event-taxonomy.md +74 -0
- package/skills/market-event-impact/references/methodology.md +98 -0
- package/skills/market-event-impact/references/research-contract.md +120 -0
- package/skills/market-event-impact/references/source-priority.md +25 -0
- package/skills/market-event-impact/scripts/adapters.py +32 -0
- package/skills/market-event-impact/scripts/asset_selector.py +120 -0
- package/skills/market-event-impact/scripts/calendars.py +179 -0
- package/skills/market-event-impact/scripts/event_resolver.py +260 -0
- package/skills/market-event-impact/scripts/event_study.py +171 -0
- package/skills/market-event-impact/scripts/market_data.py +204 -0
- package/skills/market-event-impact/scripts/market_event.py +1065 -0
- package/skills/market-event-impact/scripts/research.py +389 -0
- package/skills/market-event-impact/skill.json +9 -0
- package/skills/store-longevity-radar/scripts/__pycache__/store_longevity_download.cpython-312.pyc +0 -0
- package/src/analytics-policy.json +18 -0
- package/src/analytics.js +106 -0
- package/src/metrics.js +49 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate reproducible narrative research inputs for arbitrary market events."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from datetime import date, datetime, time as dt_time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
|
|
13
|
+
from calendars import valid_timezone
|
|
14
|
+
from event_resolver import EVENT_TYPES, infer_event_type, stable_event_id
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ResearchError(ValueError):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _source(value: Any, context: str) -> dict[str, str]:
|
|
22
|
+
if not isinstance(value, dict) or not str(value.get("name") or "").strip() or not str(value.get("url") or "").strip():
|
|
23
|
+
raise ResearchError(f"{context} needs source.name and source.url")
|
|
24
|
+
url = str(value["url"]).strip()
|
|
25
|
+
parsed = urlparse(url)
|
|
26
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
27
|
+
raise ResearchError(f"{context}.source.url must be an http(s) URL")
|
|
28
|
+
return {"name": str(value["name"]).strip(), "url": url}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _text(value: Any, context: str) -> str:
|
|
32
|
+
if not isinstance(value, str) or not value.strip():
|
|
33
|
+
raise ResearchError(f"{context} must be a non-empty string")
|
|
34
|
+
return value.strip()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _claim_list(value: Any, field: str) -> list[dict[str, Any]]:
|
|
38
|
+
if value is None:
|
|
39
|
+
return []
|
|
40
|
+
if not isinstance(value, list) or not all(isinstance(item, dict) for item in value):
|
|
41
|
+
raise ResearchError(f"{field} must be an object array")
|
|
42
|
+
result = []
|
|
43
|
+
for index, item in enumerate(value):
|
|
44
|
+
context = f"{field}[{index}]"
|
|
45
|
+
result.append({
|
|
46
|
+
"channel": _text(item.get("channel"), f"{context}.channel"),
|
|
47
|
+
"finding": _text(item.get("finding"), f"{context}.finding"),
|
|
48
|
+
"significance": _text(item.get("significance"), f"{context}.significance"),
|
|
49
|
+
"source": _source(item.get("source"), context),
|
|
50
|
+
})
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _catalysts(value: Any) -> list[dict[str, Any]]:
|
|
55
|
+
if value is None:
|
|
56
|
+
return []
|
|
57
|
+
if not isinstance(value, list) or not all(isinstance(item, dict) for item in value):
|
|
58
|
+
raise ResearchError("catalysts must be an object array")
|
|
59
|
+
result = []
|
|
60
|
+
for index, item in enumerate(value):
|
|
61
|
+
context = f"catalysts[{index}]"
|
|
62
|
+
try:
|
|
63
|
+
event_date = date.fromisoformat(str(item.get("date"))).isoformat()
|
|
64
|
+
except ValueError as exc:
|
|
65
|
+
raise ResearchError(f"{context}.date must be YYYY-MM-DD") from exc
|
|
66
|
+
time_local = item.get("time_local")
|
|
67
|
+
timezone_name = item.get("timezone")
|
|
68
|
+
if bool(time_local) != bool(timezone_name):
|
|
69
|
+
raise ResearchError(f"{context}.time_local and timezone must be provided together")
|
|
70
|
+
if time_local:
|
|
71
|
+
try:
|
|
72
|
+
parsed_time = dt_time.fromisoformat(str(time_local))
|
|
73
|
+
if parsed_time.tzinfo is not None or not valid_timezone(str(timezone_name), date.fromisoformat(event_date)):
|
|
74
|
+
raise ValueError
|
|
75
|
+
except ValueError as exc:
|
|
76
|
+
raise ResearchError(f"{context}.time_local requires local time and valid IANA timezone") from exc
|
|
77
|
+
time_local = parsed_time.isoformat(timespec="minutes" if parsed_time.second == 0 else "seconds")
|
|
78
|
+
result.append({
|
|
79
|
+
"date": event_date,
|
|
80
|
+
"time_local": time_local,
|
|
81
|
+
"timezone": timezone_name,
|
|
82
|
+
"event": _text(item.get("event"), f"{context}.event"),
|
|
83
|
+
"why_it_matters": _text(item.get("why_it_matters"), f"{context}.why_it_matters"),
|
|
84
|
+
"source": _source(item.get("source"), context),
|
|
85
|
+
})
|
|
86
|
+
result.sort(key=lambda item: (item["date"], item["time_local"] or ""))
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _transmission(value: Any) -> list[dict[str, Any]]:
|
|
91
|
+
if value is None:
|
|
92
|
+
return []
|
|
93
|
+
if not isinstance(value, list) or not all(isinstance(item, dict) for item in value):
|
|
94
|
+
raise ResearchError("transmission must be an object array")
|
|
95
|
+
allowed = {"CONFIRMED", "CONSISTENT", "MIXED", "CONTRADICTED", "UNVERIFIED"}
|
|
96
|
+
result = []
|
|
97
|
+
for index, item in enumerate(value):
|
|
98
|
+
context = f"transmission[{index}]"
|
|
99
|
+
raw_status = str(item.get("status", "")).strip().casefold()
|
|
100
|
+
status = {"confirmed": "CONFIRMED", "consistent": "CONSISTENT", "limited": "MIXED", "mixed": "MIXED", "contradicted": "CONTRADICTED", "unverified": "UNVERIFIED"}.get(raw_status, "")
|
|
101
|
+
if status not in allowed:
|
|
102
|
+
raise ResearchError(f"{context}.status must be one of {sorted(allowed)}")
|
|
103
|
+
origin = _text(item.get("from"), f"{context}.from")
|
|
104
|
+
destination = _text(item.get("to"), f"{context}.to")
|
|
105
|
+
if origin.casefold() == destination.casefold():
|
|
106
|
+
raise ResearchError(f"{context}.from and .to must differ")
|
|
107
|
+
result.append({
|
|
108
|
+
"from": origin,
|
|
109
|
+
"to": destination,
|
|
110
|
+
"status": status,
|
|
111
|
+
"evidence": _text(item.get("evidence"), f"{context}.evidence"),
|
|
112
|
+
"source": _source(item.get("source"), context),
|
|
113
|
+
})
|
|
114
|
+
keys = [(item["from"].casefold(), item["to"].casefold()) for item in result]
|
|
115
|
+
if len(keys) != len(set(keys)):
|
|
116
|
+
raise ResearchError("transmission contains duplicate steps")
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _history(value: Any) -> dict[str, Any]:
|
|
121
|
+
if value is None:
|
|
122
|
+
return {"include": False, "reason": None, "comparisons": []}
|
|
123
|
+
if not isinstance(value, dict):
|
|
124
|
+
raise ResearchError("historical_context must be an object")
|
|
125
|
+
include_raw = value.get("include", False)
|
|
126
|
+
if not isinstance(include_raw, bool):
|
|
127
|
+
raise ResearchError("historical_context.include must be true or false")
|
|
128
|
+
include = include_raw
|
|
129
|
+
comparisons = value.get("comparisons", [])
|
|
130
|
+
if not isinstance(comparisons, list) or not all(isinstance(item, dict) for item in comparisons):
|
|
131
|
+
raise ResearchError("historical_context.comparisons must be an object array")
|
|
132
|
+
normalized = []
|
|
133
|
+
for index, item in enumerate(comparisons):
|
|
134
|
+
context = f"historical_context.comparisons[{index}]"
|
|
135
|
+
try:
|
|
136
|
+
event_date = date.fromisoformat(str(item.get("date"))).isoformat()
|
|
137
|
+
except ValueError as exc:
|
|
138
|
+
raise ResearchError(f"{context}.date must be YYYY-MM-DD") from exc
|
|
139
|
+
normalized.append({
|
|
140
|
+
"date": event_date,
|
|
141
|
+
"event": _text(item.get("event"), f"{context}.event"),
|
|
142
|
+
"matching_basis": _text(item.get("matching_basis") or item.get("similarity"), f"{context}.matching_basis"),
|
|
143
|
+
"similarity": _text(item.get("similarity"), f"{context}.similarity"),
|
|
144
|
+
"difference": _text(item.get("difference"), f"{context}.difference"),
|
|
145
|
+
"source": _source(item.get("source"), context),
|
|
146
|
+
})
|
|
147
|
+
if include and not 3 <= len(normalized) <= 5:
|
|
148
|
+
raise ResearchError("historical_context.include=true needs 3 to 5 comparisons")
|
|
149
|
+
if not include and normalized:
|
|
150
|
+
raise ResearchError("historical_context comparisons require include=true")
|
|
151
|
+
return {
|
|
152
|
+
"include": include,
|
|
153
|
+
"reason": _text(value.get("reason"), "historical_context.reason") if include else None,
|
|
154
|
+
"comparisons": sorted(normalized, key=lambda item: item["date"]),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _tokens(value: Any, context: str) -> list[str]:
|
|
159
|
+
if not isinstance(value, list) or not all(isinstance(item, str) and item.strip() for item in value):
|
|
160
|
+
raise ResearchError(f"{context} must be a non-empty-string array")
|
|
161
|
+
return list(dict.fromkeys(item.strip().casefold() for item in value))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _shock_pattern(value: Any, context: str) -> dict[str, str]:
|
|
165
|
+
allowed = {"up", "down", "mixed", "flat", "wider", "tighter"}
|
|
166
|
+
if not isinstance(value, dict) or not value:
|
|
167
|
+
raise ResearchError(f"{context} must be a non-empty object")
|
|
168
|
+
result = {}
|
|
169
|
+
for raw_key, raw_direction in value.items():
|
|
170
|
+
key = _text(raw_key, f"{context} key").casefold()
|
|
171
|
+
direction = _text(raw_direction, f"{context}.{key}").casefold()
|
|
172
|
+
if direction not in allowed:
|
|
173
|
+
raise ResearchError(f"{context}.{key} must be one of {sorted(allowed)}")
|
|
174
|
+
result[key] = direction
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _event_features(value: Any, context: str = "event_features") -> dict[str, Any]:
|
|
179
|
+
if value is None:
|
|
180
|
+
return {"shock_mechanisms": [], "affected_channels": [], "market_shock_pattern": {}}
|
|
181
|
+
if not isinstance(value, dict):
|
|
182
|
+
raise ResearchError(f"{context} must be an object")
|
|
183
|
+
mechanisms = _tokens(value.get("shock_mechanisms", []), f"{context}.shock_mechanisms") if value.get("shock_mechanisms") else []
|
|
184
|
+
channels = _tokens(value.get("affected_channels", []), f"{context}.affected_channels") if value.get("affected_channels") else []
|
|
185
|
+
pattern = _shock_pattern(value["market_shock_pattern"], f"{context}.market_shock_pattern") if value.get("market_shock_pattern") else {}
|
|
186
|
+
return {"shock_mechanisms": mechanisms, "affected_channels": channels, "market_shock_pattern": pattern}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _analogue_pool(value: Any, event_date: str) -> list[dict[str, Any]]:
|
|
190
|
+
if value is None:
|
|
191
|
+
return []
|
|
192
|
+
if not isinstance(value, list) or not all(isinstance(item, dict) for item in value):
|
|
193
|
+
raise ResearchError("historical_analogue_pool must be an object array")
|
|
194
|
+
if len(value) > 50:
|
|
195
|
+
raise ResearchError("historical_analogue_pool supports at most 50 candidates")
|
|
196
|
+
result = []
|
|
197
|
+
keys = set()
|
|
198
|
+
for index, item in enumerate(value):
|
|
199
|
+
context = f"historical_analogue_pool[{index}]"
|
|
200
|
+
try:
|
|
201
|
+
candidate_date = date.fromisoformat(str(item.get("date"))).isoformat()
|
|
202
|
+
except ValueError as exc:
|
|
203
|
+
raise ResearchError(f"{context}.date must be YYYY-MM-DD") from exc
|
|
204
|
+
if candidate_date >= event_date:
|
|
205
|
+
raise ResearchError(f"{context}.date must be before the research event_date")
|
|
206
|
+
event_type = _text(item.get("event_type"), f"{context}.event_type").casefold().replace(" ", "_")
|
|
207
|
+
if event_type not in EVENT_TYPES:
|
|
208
|
+
raise ResearchError(f"{context}.event_type is unsupported")
|
|
209
|
+
features = _event_features(item, context)
|
|
210
|
+
if not features["shock_mechanisms"] or not features["affected_channels"] or not features["market_shock_pattern"]:
|
|
211
|
+
raise ResearchError(f"{context} needs shock_mechanisms, affected_channels, and market_shock_pattern")
|
|
212
|
+
candidate = {
|
|
213
|
+
"date": candidate_date,
|
|
214
|
+
"event": _text(item.get("event"), f"{context}.event"),
|
|
215
|
+
"event_type": event_type,
|
|
216
|
+
**features,
|
|
217
|
+
"similarity": _text(item.get("similarity"), f"{context}.similarity"),
|
|
218
|
+
"difference": _text(item.get("difference"), f"{context}.difference"),
|
|
219
|
+
"source": _source(item.get("source"), context),
|
|
220
|
+
}
|
|
221
|
+
key = (candidate_date, candidate["event"].casefold())
|
|
222
|
+
if key in keys:
|
|
223
|
+
raise ResearchError("historical_analogue_pool contains duplicate events")
|
|
224
|
+
keys.add(key)
|
|
225
|
+
result.append(candidate)
|
|
226
|
+
return result
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _overlap(left: set[str], right: set[str]) -> float:
|
|
230
|
+
union = left | right
|
|
231
|
+
return len(left & right) / len(union) if union else 0.0
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _rank_analogues(current: dict[str, Any], candidates: list[dict[str, Any]], event_type: str) -> dict[str, Any]:
|
|
235
|
+
ranked = []
|
|
236
|
+
for candidate in candidates:
|
|
237
|
+
mechanism_overlap = _overlap(set(current["shock_mechanisms"]), set(candidate["shock_mechanisms"]))
|
|
238
|
+
channel_overlap = _overlap(set(current["affected_channels"]), set(candidate["affected_channels"]))
|
|
239
|
+
current_pattern = current["market_shock_pattern"]
|
|
240
|
+
candidate_pattern = candidate["market_shock_pattern"]
|
|
241
|
+
shared_keys = set(current_pattern) & set(candidate_pattern)
|
|
242
|
+
pattern_union = set(current_pattern) | set(candidate_pattern)
|
|
243
|
+
pattern_overlap = (
|
|
244
|
+
sum(current_pattern[key] == candidate_pattern[key] for key in shared_keys) / len(pattern_union)
|
|
245
|
+
if pattern_union else 0.0
|
|
246
|
+
)
|
|
247
|
+
if mechanism_overlap == 0 or pattern_overlap == 0:
|
|
248
|
+
continue
|
|
249
|
+
type_match = event_type == candidate["event_type"]
|
|
250
|
+
score = round(2.0 * type_match + 3.0 * mechanism_overlap + 2.0 * channel_overlap + 4.0 * pattern_overlap, 6)
|
|
251
|
+
matching_basis = (
|
|
252
|
+
f"사건유형 {'일치' if type_match else '불일치'}, 충격 메커니즘 중복률 {mechanism_overlap:.0%}, "
|
|
253
|
+
f"영향 채널 중복률 {channel_overlap:.0%}, 관측된 시장충격 방향 일치율 {pattern_overlap:.0%}"
|
|
254
|
+
)
|
|
255
|
+
ranked.append((score, candidate["date"], candidate, matching_basis))
|
|
256
|
+
ranked.sort(key=lambda item: (-item[0], item[1], item[2]["event"].casefold()))
|
|
257
|
+
if len(ranked) < 3:
|
|
258
|
+
return {
|
|
259
|
+
"include": False,
|
|
260
|
+
"reason": "특성과 시장충격 방향을 함께 대조할 검증된 과거 후보가 3개 미만입니다.",
|
|
261
|
+
"comparisons": [],
|
|
262
|
+
}
|
|
263
|
+
comparisons = []
|
|
264
|
+
for score, _, candidate, matching_basis in ranked[:5]:
|
|
265
|
+
comparisons.append({
|
|
266
|
+
"date": candidate["date"],
|
|
267
|
+
"event": candidate["event"],
|
|
268
|
+
"matching_basis": matching_basis,
|
|
269
|
+
"similarity": candidate["similarity"],
|
|
270
|
+
"difference": candidate["difference"],
|
|
271
|
+
"score": score,
|
|
272
|
+
"source": candidate["source"],
|
|
273
|
+
})
|
|
274
|
+
return {
|
|
275
|
+
"include": True,
|
|
276
|
+
"reason": "사건 특성과 실제 시장충격 방향을 함께 점수화해 과거 사례를 정렬했습니다.",
|
|
277
|
+
"comparisons": comparisons,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def normalize_research(item: dict[str, Any]) -> dict[str, Any]:
|
|
282
|
+
try:
|
|
283
|
+
event_date = date.fromisoformat(str(item.get("event_date"))).isoformat()
|
|
284
|
+
except ValueError as exc:
|
|
285
|
+
raise ResearchError("research event_date must be YYYY-MM-DD") from exc
|
|
286
|
+
region = str(item.get("region", "")).upper()
|
|
287
|
+
if not re.fullmatch(r"[A-Z]{2,6}", region):
|
|
288
|
+
raise ResearchError("research region must be a 2-6 letter market code")
|
|
289
|
+
overview = item.get("overview") or item.get("decision")
|
|
290
|
+
if not isinstance(overview, dict):
|
|
291
|
+
raise ResearchError("research overview must be an object")
|
|
292
|
+
if overview.get("expected") is not None and not isinstance(overview.get("expected"), bool):
|
|
293
|
+
raise ResearchError("overview.expected must be true, false, or null")
|
|
294
|
+
event_name = _text(item.get("event_name") or item.get("event_family"), "research.event_name")
|
|
295
|
+
event_id = str(item.get("event_id") or "").strip().casefold() or stable_event_id(event_name)
|
|
296
|
+
if not re.fullmatch(r"[a-z0-9][a-z0-9._-]{0,95}", event_id):
|
|
297
|
+
raise ResearchError("research.event_id must use 1-96 lowercase letters, numbers, dots, underscores, or hyphens")
|
|
298
|
+
raw_event_type = item.get("event_type")
|
|
299
|
+
event_type = (_text(raw_event_type, "research.event_type").casefold().replace(" ", "_")
|
|
300
|
+
if raw_event_type is not None else infer_event_type(f"{event_name} {item.get('event_family') or ''}")[0])
|
|
301
|
+
if not event_id:
|
|
302
|
+
raise ResearchError("research.event_id is required when it cannot be derived from event_name")
|
|
303
|
+
if event_type not in EVENT_TYPES:
|
|
304
|
+
raise ResearchError(f"unsupported research event_type: {event_type}")
|
|
305
|
+
research_observed_at = item.get("observed_at")
|
|
306
|
+
if research_observed_at is not None:
|
|
307
|
+
try:
|
|
308
|
+
parsed_research_observed_at = datetime.fromisoformat(str(research_observed_at).replace("Z", "+00:00"))
|
|
309
|
+
except ValueError as exc:
|
|
310
|
+
raise ResearchError("research observed_at must be ISO 8601 with a timezone") from exc
|
|
311
|
+
if parsed_research_observed_at.tzinfo is None:
|
|
312
|
+
raise ResearchError("research observed_at must be ISO 8601 with a timezone")
|
|
313
|
+
research_observed_at = parsed_research_observed_at.isoformat()
|
|
314
|
+
normalized = {
|
|
315
|
+
"region": region,
|
|
316
|
+
"event_id": event_id,
|
|
317
|
+
"event_name": event_name,
|
|
318
|
+
"event_family": str(item.get("event_family") or "").strip().upper().replace(" ", "_") or None,
|
|
319
|
+
"event_type": event_type,
|
|
320
|
+
"event_date": event_date,
|
|
321
|
+
"observed_at": research_observed_at,
|
|
322
|
+
"event_features": _event_features(item.get("event_features")),
|
|
323
|
+
"historical_analogue_pool": _analogue_pool(item.get("historical_analogue_pool"), event_date),
|
|
324
|
+
"overview": {
|
|
325
|
+
"summary": _text(overview.get("summary"), "overview.summary"),
|
|
326
|
+
"expected": overview.get("expected") if isinstance(overview.get("expected"), bool) else None,
|
|
327
|
+
"surprise_assessment": _text(overview.get("surprise_assessment"), "overview.surprise_assessment"),
|
|
328
|
+
"classification": _text(overview.get("classification") or overview.get("event_type"), "overview.classification"),
|
|
329
|
+
"source": _source(overview.get("source"), "overview"),
|
|
330
|
+
},
|
|
331
|
+
"what_changed": _claim_list(item.get("what_changed"), "what_changed"),
|
|
332
|
+
"adapter_details": _claim_list(item.get("adapter_details"), "adapter_details"),
|
|
333
|
+
"market_interpretation": _claim_list(item.get("market_interpretation"), "market_interpretation"),
|
|
334
|
+
"transmission": _transmission(item.get("transmission")),
|
|
335
|
+
"catalysts": _catalysts(item.get("catalysts")),
|
|
336
|
+
"historical_context": _history(item.get("historical_context")),
|
|
337
|
+
}
|
|
338
|
+
if not normalized["historical_context"]["include"] and normalized["historical_analogue_pool"]:
|
|
339
|
+
normalized["historical_context"] = _rank_analogues(
|
|
340
|
+
normalized["event_features"], normalized["historical_analogue_pool"], event_type
|
|
341
|
+
)
|
|
342
|
+
if not normalized["what_changed"]:
|
|
343
|
+
raise ResearchError("research needs at least one what_changed claim")
|
|
344
|
+
if not normalized["market_interpretation"]:
|
|
345
|
+
raise ResearchError("research needs at least one market_interpretation claim")
|
|
346
|
+
if not normalized["transmission"]:
|
|
347
|
+
raise ResearchError("research needs at least one transmission step; use unverified when evidence is unavailable")
|
|
348
|
+
if len(normalized["catalysts"]) < 2:
|
|
349
|
+
raise ResearchError("research needs at least two relevant catalysts")
|
|
350
|
+
if len(normalized["catalysts"]) > 8:
|
|
351
|
+
raise ResearchError("research supports at most eight relevant catalysts")
|
|
352
|
+
catalyst_keys = [(item["date"], item["event"].casefold()) for item in normalized["catalysts"]]
|
|
353
|
+
if len(catalyst_keys) != len(set(catalyst_keys)):
|
|
354
|
+
raise ResearchError("research contains duplicate catalysts")
|
|
355
|
+
if any(item["date"] <= event_date for item in normalized["catalysts"]):
|
|
356
|
+
raise ResearchError("catalysts must be after the research event_date")
|
|
357
|
+
history = normalized["historical_context"]
|
|
358
|
+
if any(item["date"] >= event_date for item in history["comparisons"]):
|
|
359
|
+
raise ResearchError("historical comparisons must be before the research event_date")
|
|
360
|
+
return normalized
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def load_research_file(path_text: str) -> list[dict[str, Any]]:
|
|
364
|
+
path = Path(path_text).expanduser()
|
|
365
|
+
if not path.is_file():
|
|
366
|
+
raise ResearchError(f"research file not found: {path}")
|
|
367
|
+
try:
|
|
368
|
+
payload = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
369
|
+
except json.JSONDecodeError as exc:
|
|
370
|
+
raise ResearchError(f"invalid research JSON: {exc}") from exc
|
|
371
|
+
items = payload.get("research") if isinstance(payload, dict) else payload
|
|
372
|
+
if not isinstance(items, list) or not all(isinstance(item, dict) for item in items):
|
|
373
|
+
raise ResearchError("research file must be an array or contain a research array")
|
|
374
|
+
normalized = [normalize_research(item) for item in items]
|
|
375
|
+
keys = [(item["event_id"], item["event_date"]) for item in normalized]
|
|
376
|
+
if len(keys) != len(set(keys)):
|
|
377
|
+
raise ResearchError("research file contains duplicate event keys")
|
|
378
|
+
return normalized
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def select_research(items: list[dict[str, Any]], event: dict[str, Any]) -> dict[str, Any]:
|
|
382
|
+
matches = [item for item in items if item["event_id"] == event["event_id"] and item["event_date"] == event["event_date"]]
|
|
383
|
+
if not matches:
|
|
384
|
+
matches = [item for item in items if item["event_name"].casefold() == event["event_name"].casefold() and item["event_date"] == event["event_date"]]
|
|
385
|
+
if not matches and event.get("event_family"):
|
|
386
|
+
matches = [item for item in items if item.get("event_family") == event.get("event_family") and item["region"] == event["region"] and item["event_date"] == event["event_date"]]
|
|
387
|
+
if len(matches) != 1:
|
|
388
|
+
raise ResearchError(f"research file must contain exactly one matching event; found {len(matches)}")
|
|
389
|
+
return matches[0]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "market-event-impact",
|
|
3
|
+
"description": "GS Quant의 event_study·event_impact_analysis·CalendarAlignment 구조에서 영감을 받아, 정례 발표와 비정형 사건의 핵심 변화 및 시장 해석을 공식자료·당시 보도·검증 가격으로 정리한다. 교차자산 전달경로를 살피고 이벤트 스터디는 보조 증거로 제시하며, 유사사례와 다음 촉매를 한국어로 설명한다. Use for named market-moving events. Not for live quotes, forecasts, or investment advice.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"lookup",
|
|
6
|
+
"browser"
|
|
7
|
+
],
|
|
8
|
+
"frontmatter": "name: market-event-impact\ndescription: GS Quant의 event_study·event_impact_analysis·CalendarAlignment 구조에서 영감을 받아, 정례 발표와 비정형 사건의 핵심 변화 및 시장 해석을 공식자료·당시 보도·검증 가격으로 정리한다. 교차자산 전달경로를 살피고 이벤트 스터디는 보조 증거로 제시하며, 유사사례와 다음 촉매를 한국어로 설명한다. Use for named market-moving events. Not for live quotes, forecasts, or investment advice.\nlicense: MIT\nmetadata:\n category: finance\n locale: ko-KR\n phase: v1"
|
|
9
|
+
}
|
package/skills/store-longevity-radar/scripts/__pycache__/store_longevity_download.cpython-312.pyc
CHANGED
|
Binary file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"purpose": "usage_statistics_only",
|
|
3
|
+
"event": "cli_invocation",
|
|
4
|
+
"collected_properties": [
|
|
5
|
+
"command",
|
|
6
|
+
"skill_name",
|
|
7
|
+
"cli_version",
|
|
8
|
+
"runtime_mode",
|
|
9
|
+
"success",
|
|
10
|
+
"analytics_schema_version"
|
|
11
|
+
],
|
|
12
|
+
"excluded_data": [
|
|
13
|
+
"command_arguments",
|
|
14
|
+
"command_input",
|
|
15
|
+
"file_contents"
|
|
16
|
+
],
|
|
17
|
+
"opt_out_environment_variable": "KSKILL_ANALYTICS_DISABLED"
|
|
18
|
+
}
|
package/src/analytics.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const os = require("node:os");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const { version: cliVersion } = require("../package.json");
|
|
8
|
+
const policy = require("./analytics-policy.json");
|
|
9
|
+
|
|
10
|
+
const DEFAULT_HOST = "https://us.i.posthog.com";
|
|
11
|
+
const DEFAULT_PROJECT_API_KEY =
|
|
12
|
+
"phc_xlJYWKplsT2UNHng9eDULTGddlq0RoTuE8Dh64nrpmL";
|
|
13
|
+
const ANALYTICS_SCHEMA_VERSION = 1;
|
|
14
|
+
const DISABLED_VALUES = new Set(["1", "true", "yes", "on"]);
|
|
15
|
+
|
|
16
|
+
function isDisabled(env) {
|
|
17
|
+
return DISABLED_VALUES.has(String(env.KSKILL_ANALYTICS_DISABLED || "").toLowerCase());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function analyticsIdPath(env) {
|
|
21
|
+
return (
|
|
22
|
+
env.KSKILL_ANALYTICS_ID_FILE ||
|
|
23
|
+
path.join(env.KSKILL_CONFIG_DIR || path.join(os.homedir(), ".config", "k-skill"), "analytics-id")
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readOrCreateAnalyticsId(env, randomUUID = crypto.randomUUID) {
|
|
28
|
+
if (env.KSKILL_ANALYTICS_ID) return env.KSKILL_ANALYTICS_ID;
|
|
29
|
+
|
|
30
|
+
const idPath = analyticsIdPath(env);
|
|
31
|
+
try {
|
|
32
|
+
const existing = fs.readFileSync(idPath, "utf8").trim();
|
|
33
|
+
if (existing) return existing;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error.code !== "ENOENT") return randomUUID();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const generated = randomUUID();
|
|
39
|
+
try {
|
|
40
|
+
fs.mkdirSync(path.dirname(idPath), { recursive: true, mode: 0o700 });
|
|
41
|
+
fs.writeFileSync(idPath, `${generated}\n`, { encoding: "utf8", mode: 0o600 });
|
|
42
|
+
} catch {
|
|
43
|
+
// Analytics must never make the CLI command fail.
|
|
44
|
+
}
|
|
45
|
+
return generated;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function analyticsConfig(env = process.env) {
|
|
49
|
+
if (isDisabled(env)) return null;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
apiKey: env.POSTHOG_API_KEY || DEFAULT_PROJECT_API_KEY,
|
|
53
|
+
host: (env.POSTHOG_HOST || DEFAULT_HOST).replace(/\/+$/, ""),
|
|
54
|
+
distinctId: readOrCreateAnalyticsId(env),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildInvocationEvent({ command, skillName, runtimeMode, success }) {
|
|
59
|
+
return {
|
|
60
|
+
event: policy.event,
|
|
61
|
+
properties: {
|
|
62
|
+
command: command || null,
|
|
63
|
+
skill_name: skillName || null,
|
|
64
|
+
cli_version: cliVersion,
|
|
65
|
+
runtime_mode: runtimeMode,
|
|
66
|
+
success: Boolean(success),
|
|
67
|
+
analytics_schema_version: ANALYTICS_SCHEMA_VERSION,
|
|
68
|
+
$process_person_profile: false,
|
|
69
|
+
disable_geoip: true,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function captureInvocation(details, env = process.env, fetchImpl = fetch) {
|
|
75
|
+
const config = analyticsConfig(env);
|
|
76
|
+
if (!config) return false;
|
|
77
|
+
|
|
78
|
+
const event = buildInvocationEvent(details);
|
|
79
|
+
const payload = JSON.stringify({
|
|
80
|
+
api_key: config.apiKey,
|
|
81
|
+
distinct_id: config.distinctId,
|
|
82
|
+
event: event.event,
|
|
83
|
+
properties: event.properties,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const response = await fetchImpl(`${config.host}/i/v0/e/`, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: { "content-type": "application/json" },
|
|
90
|
+
body: payload,
|
|
91
|
+
signal: AbortSignal.timeout(1500),
|
|
92
|
+
});
|
|
93
|
+
return response.ok;
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
ANALYTICS_SCHEMA_VERSION,
|
|
101
|
+
DEFAULT_PROJECT_API_KEY,
|
|
102
|
+
analyticsConfig,
|
|
103
|
+
buildInvocationEvent,
|
|
104
|
+
captureInvocation,
|
|
105
|
+
readOrCreateAnalyticsId,
|
|
106
|
+
};
|
package/src/metrics.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function inWindow(timestamp, start, end) {
|
|
4
|
+
const value = new Date(timestamp).getTime();
|
|
5
|
+
return Number.isFinite(value) && value >= start && value < end;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function uniqueIds(events) {
|
|
9
|
+
return new Set(events.map((event) => event.distinct_id).filter(Boolean)).size;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function deriveUsageMetrics(events, end = Date.now()) {
|
|
13
|
+
const windows = {
|
|
14
|
+
daily: 24 * 60 * 60 * 1000,
|
|
15
|
+
weekly: 7 * 24 * 60 * 60 * 1000,
|
|
16
|
+
monthly: 30 * 24 * 60 * 60 * 1000,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const metrics = {};
|
|
20
|
+
for (const [name, duration] of Object.entries(windows)) {
|
|
21
|
+
const selected = events.filter((event) =>
|
|
22
|
+
inWindow(event.timestamp, end - duration, end),
|
|
23
|
+
);
|
|
24
|
+
const uniqueUsers = uniqueIds(selected);
|
|
25
|
+
metrics[name] = {
|
|
26
|
+
calls: selected.length,
|
|
27
|
+
uniqueUsers,
|
|
28
|
+
executionsPerUniqueUser: uniqueUsers ? selected.length / uniqueUsers : 0,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const recentIds = new Set(
|
|
33
|
+
events
|
|
34
|
+
.filter((event) => inWindow(event.timestamp, end - windows.weekly, end))
|
|
35
|
+
.map((event) => event.distinct_id)
|
|
36
|
+
.filter(Boolean),
|
|
37
|
+
);
|
|
38
|
+
const priorIds = new Set(
|
|
39
|
+
events
|
|
40
|
+
.filter((event) => inWindow(event.timestamp, end - 2 * windows.weekly, end - windows.weekly))
|
|
41
|
+
.map((event) => event.distinct_id)
|
|
42
|
+
.filter(Boolean),
|
|
43
|
+
);
|
|
44
|
+
metrics.recurrentUsers = [...recentIds].filter((id) => priorIds.has(id)).length;
|
|
45
|
+
|
|
46
|
+
return metrics;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { deriveUsageMetrics };
|