@apolloyh/apollo-agent 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/brand.d.ts +1 -1
- package/dist/brand.js +1 -1
- package/dist/cli-input.d.ts +16 -0
- package/dist/cli-input.d.ts.map +1 -0
- package/dist/cli-input.js +68 -0
- package/dist/index.js +39 -37
- package/dist/skills/skills.d.ts.map +1 -1
- package/dist/skills/skills.js +9 -10
- package/dist/tools/builtin.js +2 -2
- package/package.json +1 -2
- package/src/skills/tech-research-skill/SKILL.md +0 -200
- package/src/skills/tech-research-skill/agents/openai.yaml +0 -4
- package/src/skills/tech-research-skill/repo-analyze +0 -435
- package/src/skills/tech-research-skill/repo-fetch +0 -310
- package/src/skills/tech-research-skill/report-generate +0 -254
|
@@ -1,435 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
import argparse
|
|
3
|
-
import datetime as dt
|
|
4
|
-
import json
|
|
5
|
-
import math
|
|
6
|
-
import sys
|
|
7
|
-
|
|
8
|
-
VERSION = "1.1.0"
|
|
9
|
-
MODEL = "local-heuristic-v1"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def now_iso():
|
|
13
|
-
return dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def fail(code, message, details=None, exit_code=1):
|
|
17
|
-
payload = {
|
|
18
|
-
"ok": False,
|
|
19
|
-
"error": {
|
|
20
|
-
"code": code,
|
|
21
|
-
"message": message,
|
|
22
|
-
"details": details or {},
|
|
23
|
-
},
|
|
24
|
-
}
|
|
25
|
-
print(json.dumps(payload, ensure_ascii=False, sort_keys=True), file=sys.stderr)
|
|
26
|
-
raise SystemExit(exit_code)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def read_stdin_json(max_bytes):
|
|
30
|
-
raw = sys.stdin.buffer.read(max_bytes + 1)
|
|
31
|
-
if not raw:
|
|
32
|
-
fail("invalid_input", "stdin is empty")
|
|
33
|
-
if len(raw) > max_bytes:
|
|
34
|
-
fail("invalid_input", "stdin exceeds --max-input-bytes", {"max_input_bytes": max_bytes})
|
|
35
|
-
try:
|
|
36
|
-
return json.loads(raw.decode("utf-8"))
|
|
37
|
-
except UnicodeDecodeError as exc:
|
|
38
|
-
fail("invalid_input", "stdin must be UTF-8", {"error": str(exc)})
|
|
39
|
-
except json.JSONDecodeError as exc:
|
|
40
|
-
fail("invalid_json", "stdin must be valid JSON", {"error": str(exc)})
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def validate_fetch_payload(payload):
|
|
44
|
-
if not isinstance(payload, dict):
|
|
45
|
-
fail("invalid_input", "input JSON must be an object")
|
|
46
|
-
if payload.get("ok") is not True or payload.get("source") != "repo-fetch":
|
|
47
|
-
fail("invalid_input", "input must be successful repo-fetch output")
|
|
48
|
-
mode = payload.get("mode", "repository")
|
|
49
|
-
if mode == "repository" and not isinstance(payload.get("repository"), dict):
|
|
50
|
-
fail("invalid_input", "repository input missing repository object")
|
|
51
|
-
if mode == "search" and not isinstance(payload.get("search"), dict):
|
|
52
|
-
fail("invalid_input", "search input missing search object")
|
|
53
|
-
if mode not in {"repository", "search"}:
|
|
54
|
-
fail("invalid_input", "unsupported repo-fetch mode", {"mode": mode})
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def parse_time(value):
|
|
58
|
-
if not value or not isinstance(value, str):
|
|
59
|
-
return None
|
|
60
|
-
normalized = value.replace("Z", "+00:00")
|
|
61
|
-
try:
|
|
62
|
-
parsed = dt.datetime.fromisoformat(normalized)
|
|
63
|
-
except ValueError:
|
|
64
|
-
return None
|
|
65
|
-
if parsed.tzinfo is None:
|
|
66
|
-
parsed = parsed.replace(tzinfo=dt.timezone.utc)
|
|
67
|
-
return parsed.astimezone(dt.timezone.utc)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def days_since(value):
|
|
71
|
-
parsed = parse_time(value)
|
|
72
|
-
if parsed is None:
|
|
73
|
-
return None
|
|
74
|
-
return max(0, (dt.datetime.now(dt.timezone.utc) - parsed).days)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def clamp(value, minimum=0, maximum=100):
|
|
78
|
-
return max(minimum, min(maximum, int(round(value))))
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def count_recent(items, key, max_days):
|
|
82
|
-
count = 0
|
|
83
|
-
for item in items or []:
|
|
84
|
-
if isinstance(item, dict):
|
|
85
|
-
age = days_since(item.get(key))
|
|
86
|
-
if age is not None and age <= max_days:
|
|
87
|
-
count += 1
|
|
88
|
-
return count
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def stars_points(stars):
|
|
92
|
-
stars = stars or 0
|
|
93
|
-
if stars <= 0:
|
|
94
|
-
return 0
|
|
95
|
-
return min(22, math.log10(stars + 1) * 6)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def forks_points(forks):
|
|
99
|
-
forks = forks or 0
|
|
100
|
-
if forks <= 0:
|
|
101
|
-
return 0
|
|
102
|
-
return min(10, math.log10(forks + 1) * 4)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def recency_status(age_days):
|
|
106
|
-
if age_days is None:
|
|
107
|
-
return "watch", "no pushed_at timestamp was captured"
|
|
108
|
-
if age_days <= 30:
|
|
109
|
-
return "good", f"last push was {age_days} days ago"
|
|
110
|
-
if age_days <= 180:
|
|
111
|
-
return "watch", f"last push was {age_days} days ago"
|
|
112
|
-
return "poor", f"last push was {age_days} days ago"
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def score_repository(repo, data):
|
|
116
|
-
stars = repo.get("stargazers_count") or 0
|
|
117
|
-
forks = repo.get("forks_count") or 0
|
|
118
|
-
open_issues = repo.get("open_issues_count") or 0
|
|
119
|
-
push_age = days_since(repo.get("pushed_at"))
|
|
120
|
-
commits = data.get("commits") if isinstance(data, dict) else []
|
|
121
|
-
issues = data.get("issues") if isinstance(data, dict) else []
|
|
122
|
-
|
|
123
|
-
score = 38
|
|
124
|
-
score += stars_points(stars)
|
|
125
|
-
score += forks_points(forks)
|
|
126
|
-
|
|
127
|
-
if push_age is None:
|
|
128
|
-
score -= 2
|
|
129
|
-
elif push_age <= 30:
|
|
130
|
-
score += 18
|
|
131
|
-
elif push_age <= 90:
|
|
132
|
-
score += 12
|
|
133
|
-
elif push_age <= 180:
|
|
134
|
-
score += 6
|
|
135
|
-
elif push_age <= 365:
|
|
136
|
-
score -= 4
|
|
137
|
-
else:
|
|
138
|
-
score -= 14
|
|
139
|
-
|
|
140
|
-
if commits:
|
|
141
|
-
recent_commits = count_recent(commits, "date", 90)
|
|
142
|
-
score += min(10, len(commits) / 3)
|
|
143
|
-
if recent_commits:
|
|
144
|
-
score += min(8, recent_commits * 2)
|
|
145
|
-
else:
|
|
146
|
-
score -= 4
|
|
147
|
-
|
|
148
|
-
if issues:
|
|
149
|
-
recently_updated_issues = count_recent(issues, "updated_at", 90)
|
|
150
|
-
score += min(6, recently_updated_issues)
|
|
151
|
-
|
|
152
|
-
if repo.get("archived"):
|
|
153
|
-
score -= 35
|
|
154
|
-
if repo.get("disabled"):
|
|
155
|
-
score -= 25
|
|
156
|
-
if not repo.get("license"):
|
|
157
|
-
score -= 5
|
|
158
|
-
if stars >= 1000 and forks >= 100:
|
|
159
|
-
score += 5
|
|
160
|
-
if open_issues > max(200, stars * 0.12):
|
|
161
|
-
score -= 5
|
|
162
|
-
|
|
163
|
-
return clamp(score)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def investment_fit(score, risks):
|
|
167
|
-
high_risks = sum(1 for risk in risks if risk.get("severity") == "high")
|
|
168
|
-
if score >= 80 and high_risks == 0:
|
|
169
|
-
return "strong"
|
|
170
|
-
if score >= 65:
|
|
171
|
-
return "promising"
|
|
172
|
-
if score >= 45:
|
|
173
|
-
return "watchlist"
|
|
174
|
-
return "avoid"
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def analyze_repository(fetch_payload):
|
|
178
|
-
repo = fetch_payload.get("repository") or {}
|
|
179
|
-
data = fetch_payload.get("data") or {}
|
|
180
|
-
issues = data.get("issues") or []
|
|
181
|
-
commits = data.get("commits") or []
|
|
182
|
-
name = repo.get("full_name") or repo.get("html_url") or "unknown repository"
|
|
183
|
-
stars = repo.get("stargazers_count") or 0
|
|
184
|
-
forks = repo.get("forks_count") or 0
|
|
185
|
-
open_issues = repo.get("open_issues_count") or 0
|
|
186
|
-
push_age = days_since(repo.get("pushed_at"))
|
|
187
|
-
activity_status, activity_detail = recency_status(push_age)
|
|
188
|
-
recent_commits = count_recent(commits, "date", 90)
|
|
189
|
-
updated_issues = count_recent(issues, "updated_at", 90)
|
|
190
|
-
|
|
191
|
-
signals = [
|
|
192
|
-
{
|
|
193
|
-
"name": "activity",
|
|
194
|
-
"status": activity_status,
|
|
195
|
-
"detail": activity_detail
|
|
196
|
-
+ (f"; {recent_commits} captured commits in the last 90 days" if commits else "; commit dataset was not captured"),
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
"name": "community",
|
|
200
|
-
"status": "good" if stars >= 1000 else "watch" if stars >= 100 else "poor",
|
|
201
|
-
"detail": f"{stars} stars, {forks} forks, {open_issues} open issues",
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
"name": "maintenance",
|
|
205
|
-
"status": "poor" if repo.get("archived") or repo.get("disabled") else "good" if updated_issues or recent_commits else "watch",
|
|
206
|
-
"detail": f"archived={bool(repo.get('archived'))}, disabled={bool(repo.get('disabled'))}, license={repo.get('license') or 'not captured'}",
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
"name": "risk",
|
|
210
|
-
"status": "poor" if repo.get("archived") or repo.get("disabled") else "watch" if not repo.get("license") else "good",
|
|
211
|
-
"detail": "primary risk flags are based on archive/disabled/license metadata and captured issue activity",
|
|
212
|
-
},
|
|
213
|
-
{
|
|
214
|
-
"name": "momentum",
|
|
215
|
-
"status": "good" if (push_age is not None and push_age <= 30 and stars >= 100) else "watch" if push_age is not None and push_age <= 180 else "poor",
|
|
216
|
-
"detail": f"recent issue updates captured: {updated_issues}; recent commits captured: {recent_commits}",
|
|
217
|
-
},
|
|
218
|
-
]
|
|
219
|
-
|
|
220
|
-
risks = []
|
|
221
|
-
if repo.get("archived"):
|
|
222
|
-
risks.append({"severity": "high", "title": "Repository is archived", "detail": "GitHub metadata marks the repository as archived."})
|
|
223
|
-
if repo.get("disabled"):
|
|
224
|
-
risks.append({"severity": "high", "title": "Repository is disabled", "detail": "GitHub metadata marks the repository as disabled."})
|
|
225
|
-
if push_age is None:
|
|
226
|
-
risks.append({"severity": "medium", "title": "Missing activity timestamp", "detail": "No pushed_at value was captured."})
|
|
227
|
-
elif push_age > 365:
|
|
228
|
-
risks.append({"severity": "high", "title": "Long inactivity window", "detail": f"Last push was {push_age} days ago."})
|
|
229
|
-
elif push_age > 180:
|
|
230
|
-
risks.append({"severity": "medium", "title": "Activity is slowing", "detail": f"Last push was {push_age} days ago."})
|
|
231
|
-
if not repo.get("license"):
|
|
232
|
-
risks.append({"severity": "medium", "title": "License not captured", "detail": "The repository metadata did not include an SPDX license."})
|
|
233
|
-
if not commits:
|
|
234
|
-
risks.append({"severity": "low", "title": "No commit sample", "detail": "Run repo-fetch with --include commits for stronger activity evidence."})
|
|
235
|
-
if not issues:
|
|
236
|
-
risks.append({"severity": "low", "title": "No issue sample", "detail": "Run repo-fetch with --include issues for stronger maintenance evidence."})
|
|
237
|
-
if not risks:
|
|
238
|
-
risks.append({"severity": "low", "title": "No major metadata risk", "detail": "No high-risk flags were found in the captured GitHub metadata."})
|
|
239
|
-
|
|
240
|
-
score = score_repository(repo, data)
|
|
241
|
-
fit = investment_fit(score, risks)
|
|
242
|
-
summary = (
|
|
243
|
-
f"{name} has {stars} stars and {forks} forks. "
|
|
244
|
-
f"Captured metadata indicates {activity_detail}; optional datasets include "
|
|
245
|
-
f"{', '.join(sorted(data.keys())) if data else 'repository metadata only'}. "
|
|
246
|
-
"This local score is a deterministic signal summary, not a final strategic judgment."
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
recommendations = [
|
|
250
|
-
{
|
|
251
|
-
"priority": "high" if score < 60 else "medium",
|
|
252
|
-
"action": "Review maintainers, releases, and recent pull requests before relying on the project.",
|
|
253
|
-
"reason": "The local analyzer only sees repository metadata plus optional issue/commit samples.",
|
|
254
|
-
},
|
|
255
|
-
{
|
|
256
|
-
"priority": "medium",
|
|
257
|
-
"action": "Compare implementation approach against adjacent repositories in the same topic.",
|
|
258
|
-
"reason": "Relative positioning is more useful than a standalone repository score.",
|
|
259
|
-
},
|
|
260
|
-
]
|
|
261
|
-
if not commits or not issues:
|
|
262
|
-
recommendations.append(
|
|
263
|
-
{
|
|
264
|
-
"priority": "medium",
|
|
265
|
-
"action": "Fetch issues and commits for a stronger health read.",
|
|
266
|
-
"reason": "Missing optional datasets reduce confidence in maintenance and momentum signals.",
|
|
267
|
-
}
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
return {
|
|
271
|
-
"health_score": score,
|
|
272
|
-
"summary": summary,
|
|
273
|
-
"signals": signals,
|
|
274
|
-
"risks": risks,
|
|
275
|
-
"recommendations": recommendations,
|
|
276
|
-
"investment_notes": {
|
|
277
|
-
"fit": fit,
|
|
278
|
-
"rationale": f"Local repository health score is {score}/100. The agent should combine this with market, competitor, and product evidence before making a final call.",
|
|
279
|
-
"diligence_questions": [
|
|
280
|
-
"Are maintainers still shipping releases or accepting pull requests?",
|
|
281
|
-
"Do users report production adoption, benchmarks, or deployment constraints?",
|
|
282
|
-
"What commercial or open-source alternatives solve the same job better?",
|
|
283
|
-
],
|
|
284
|
-
},
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def analyze_search(fetch_payload):
|
|
289
|
-
search = fetch_payload.get("search") or {}
|
|
290
|
-
items = [item for item in search.get("items") or [] if isinstance(item, dict)]
|
|
291
|
-
query = search.get("query") or "unknown query"
|
|
292
|
-
total_count = search.get("total_count") or 0
|
|
293
|
-
top_items = items[:5]
|
|
294
|
-
top_stars = max([(item.get("stargazers_count") or 0) for item in items] or [0])
|
|
295
|
-
recent_count = sum(1 for item in items if (days_since(item.get("pushed_at")) or 999999) <= 180)
|
|
296
|
-
|
|
297
|
-
score = 35
|
|
298
|
-
score += min(20, math.log10(total_count + 1) * 5 if total_count else 0)
|
|
299
|
-
score += min(25, math.log10(top_stars + 1) * 6 if top_stars else 0)
|
|
300
|
-
score += min(20, recent_count * 3)
|
|
301
|
-
if search.get("incomplete_results"):
|
|
302
|
-
score -= 5
|
|
303
|
-
score = clamp(score)
|
|
304
|
-
|
|
305
|
-
leaders = ", ".join(
|
|
306
|
-
f"{item.get('full_name')} ({item.get('stargazers_count') or 0} stars)"
|
|
307
|
-
for item in top_items
|
|
308
|
-
if item.get("full_name")
|
|
309
|
-
)
|
|
310
|
-
if not leaders:
|
|
311
|
-
leaders = "no repositories captured"
|
|
312
|
-
|
|
313
|
-
risks = [
|
|
314
|
-
{
|
|
315
|
-
"severity": "medium",
|
|
316
|
-
"title": "Search result only",
|
|
317
|
-
"detail": "Search payloads do not include issue or commit samples; fetch representative repositories before final judgment.",
|
|
318
|
-
}
|
|
319
|
-
]
|
|
320
|
-
if search.get("incomplete_results"):
|
|
321
|
-
risks.append({"severity": "medium", "title": "Incomplete GitHub search", "detail": "GitHub marked the search result as incomplete."})
|
|
322
|
-
if not items:
|
|
323
|
-
risks.append({"severity": "high", "title": "No repositories captured", "detail": "The search query did not return usable repository items."})
|
|
324
|
-
|
|
325
|
-
return {
|
|
326
|
-
"health_score": score,
|
|
327
|
-
"summary": f"GitHub search `{query}` returned {total_count} total matches. Leading captured repositories: {leaders}. This is a local landscape signal, not a final market analysis.",
|
|
328
|
-
"signals": [
|
|
329
|
-
{
|
|
330
|
-
"name": "community",
|
|
331
|
-
"status": "good" if top_stars >= 1000 else "watch" if top_stars >= 100 else "poor",
|
|
332
|
-
"detail": f"top captured repository has {top_stars} stars",
|
|
333
|
-
},
|
|
334
|
-
{
|
|
335
|
-
"name": "activity",
|
|
336
|
-
"status": "good" if recent_count >= 5 else "watch" if recent_count else "poor",
|
|
337
|
-
"detail": f"{recent_count} captured repositories were pushed within the last 180 days",
|
|
338
|
-
},
|
|
339
|
-
{
|
|
340
|
-
"name": "momentum",
|
|
341
|
-
"status": "good" if score >= 70 else "watch" if score >= 45 else "poor",
|
|
342
|
-
"detail": f"search-level local score is {score}/100 across {len(items)} captured repositories",
|
|
343
|
-
},
|
|
344
|
-
],
|
|
345
|
-
"risks": risks,
|
|
346
|
-
"recommendations": [
|
|
347
|
-
{
|
|
348
|
-
"priority": "high",
|
|
349
|
-
"action": "Fetch 3-5 representative repositories with issues and commits.",
|
|
350
|
-
"reason": "Repository details are required for evidence-backed health and implementation analysis.",
|
|
351
|
-
},
|
|
352
|
-
{
|
|
353
|
-
"priority": "medium",
|
|
354
|
-
"action": "Use multiple adjacent keyword searches before choosing competitors.",
|
|
355
|
-
"reason": "One GitHub query can miss projects that use different positioning language.",
|
|
356
|
-
},
|
|
357
|
-
],
|
|
358
|
-
"investment_notes": {
|
|
359
|
-
"fit": investment_fit(score, risks),
|
|
360
|
-
"rationale": f"The search landscape score is {score}/100 based on captured repository count, top stars, and recency. The agent should synthesize final potential and competitor analysis.",
|
|
361
|
-
"diligence_questions": [
|
|
362
|
-
"Which captured repositories represent production-ready infrastructure versus demos?",
|
|
363
|
-
"Which projects are substitutes, complements, or direct competitors?",
|
|
364
|
-
"Are commercial vendors creating adoption that open-source projects can leverage?",
|
|
365
|
-
],
|
|
366
|
-
},
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
def build_evidence_sources(fetch_payload):
|
|
371
|
-
repositories = []
|
|
372
|
-
repo = fetch_payload.get("repository") or {}
|
|
373
|
-
if repo.get("full_name") or repo.get("html_url"):
|
|
374
|
-
repositories.append(
|
|
375
|
-
{
|
|
376
|
-
"name": repo.get("full_name") or repo.get("html_url"),
|
|
377
|
-
"url": repo.get("html_url"),
|
|
378
|
-
}
|
|
379
|
-
)
|
|
380
|
-
|
|
381
|
-
search = fetch_payload.get("search") or {}
|
|
382
|
-
for item in search.get("items") or []:
|
|
383
|
-
if isinstance(item, dict) and (item.get("full_name") or item.get("html_url")):
|
|
384
|
-
repositories.append(
|
|
385
|
-
{
|
|
386
|
-
"name": item.get("full_name") or item.get("html_url"),
|
|
387
|
-
"url": item.get("html_url"),
|
|
388
|
-
}
|
|
389
|
-
)
|
|
390
|
-
|
|
391
|
-
return {
|
|
392
|
-
"github_search_query": search.get("query"),
|
|
393
|
-
"repositories": repositories,
|
|
394
|
-
"web_pages": [],
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
def main():
|
|
399
|
-
parser = argparse.ArgumentParser(description="Summarize repo-fetch JSON with local deterministic heuristics. No LLM or API credentials are used.")
|
|
400
|
-
parser.add_argument("--max-input-bytes", type=int, default=200000, help="Maximum stdin size. Default: 200000.")
|
|
401
|
-
args = parser.parse_args()
|
|
402
|
-
|
|
403
|
-
if args.max_input_bytes < 1:
|
|
404
|
-
fail("invalid_arguments", "--max-input-bytes must be at least 1", {"max_input_bytes": args.max_input_bytes})
|
|
405
|
-
|
|
406
|
-
fetch_payload = read_stdin_json(args.max_input_bytes)
|
|
407
|
-
validate_fetch_payload(fetch_payload)
|
|
408
|
-
|
|
409
|
-
mode = fetch_payload.get("mode", "repository")
|
|
410
|
-
analysis = analyze_search(fetch_payload) if mode == "search" else analyze_repository(fetch_payload)
|
|
411
|
-
|
|
412
|
-
repo = fetch_payload.get("repository", {})
|
|
413
|
-
search = fetch_payload.get("search", {})
|
|
414
|
-
subject = repo.get("full_name") or f"github search: {search.get('query')}"
|
|
415
|
-
payload = {
|
|
416
|
-
"ok": True,
|
|
417
|
-
"source": "repo-analyze",
|
|
418
|
-
"version": VERSION,
|
|
419
|
-
"analyzed_at": now_iso(),
|
|
420
|
-
"model": MODEL,
|
|
421
|
-
"provider": "local-deterministic",
|
|
422
|
-
"input": {
|
|
423
|
-
"repository_full_name": subject,
|
|
424
|
-
"mode": mode,
|
|
425
|
-
"fetched_at": fetch_payload.get("fetched_at"),
|
|
426
|
-
"included": sorted((fetch_payload.get("data") or {}).keys()),
|
|
427
|
-
},
|
|
428
|
-
"evidence_sources": build_evidence_sources(fetch_payload),
|
|
429
|
-
"analysis": analysis,
|
|
430
|
-
}
|
|
431
|
-
print(json.dumps(payload, ensure_ascii=False, sort_keys=True))
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
if __name__ == "__main__":
|
|
435
|
-
main()
|