@adgine/geo-cli 1.2.8
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/README.md +93 -0
- package/bin/adgine-geo.js +224 -0
- package/package.json +38 -0
- package/skills/.env.example +11 -0
- package/skills/README.md +247 -0
- package/skills/SKILL.md +117 -0
- package/skills/VERSION +1 -0
- package/skills/adgine-geo-aiagent/SKILL.md +200 -0
- package/skills/adgine-geo-aiagent/scripts/_client.py +341 -0
- package/skills/adgine-geo-aiagent/scripts/bot_traffic.py +278 -0
- package/skills/adgine-geo-aiagent/scripts/human_traffic.py +357 -0
- package/skills/adgine-geo-aiagent/scripts/page_analytics.py +317 -0
- package/skills/adgine-geo-aiagent/scripts/page_detail.py +273 -0
- package/skills/adgine-geo-analytics/API.md +89 -0
- package/skills/adgine-geo-analytics/SKILL.md +191 -0
- package/skills/adgine-geo-analytics/scripts/_client.py +341 -0
- package/skills/adgine-geo-analytics/scripts/get_overview.py +129 -0
- package/skills/adgine-geo-billing/SKILL.md +118 -0
- package/skills/adgine-geo-billing/scripts/_client.py +341 -0
- package/skills/adgine-geo-billing/scripts/get_credits.py +51 -0
- package/skills/adgine-geo-billing/scripts/get_credits_pricing.py +52 -0
- package/skills/adgine-geo-billing/scripts/get_subscription.py +48 -0
- package/skills/adgine-geo-billing/scripts/list_plans.py +66 -0
- package/skills/adgine-geo-brand/SKILL.md +193 -0
- package/skills/adgine-geo-brand/WORKFLOW.md +66 -0
- package/skills/adgine-geo-brand/scripts/_client.py +341 -0
- package/skills/adgine-geo-brand/scripts/generate_brand.py +95 -0
- package/skills/adgine-geo-brand/scripts/get_brand.py +67 -0
- package/skills/adgine-geo-brand/scripts/list_jobs.py +153 -0
- package/skills/adgine-geo-brand/scripts/update_brand.py +61 -0
- package/skills/adgine-geo-citation/SKILL.md +220 -0
- package/skills/adgine-geo-citation/scripts/_client.py +341 -0
- package/skills/adgine-geo-citation/scripts/create_tests.py +62 -0
- package/skills/adgine-geo-citation/scripts/get_aggregate.py +108 -0
- package/skills/adgine-geo-citation/scripts/get_results.py +179 -0
- package/skills/adgine-geo-content/SKILL.md +224 -0
- package/skills/adgine-geo-content/WORKFLOW.md +100 -0
- package/skills/adgine-geo-content/scripts/_client.py +341 -0
- package/skills/adgine-geo-content/scripts/generate_article.py +108 -0
- package/skills/adgine-geo-content/scripts/generate_outline.py +113 -0
- package/skills/adgine-geo-content/scripts/generate_titles.py +51 -0
- package/skills/adgine-geo-content/scripts/list_content.py +65 -0
- package/skills/adgine-geo-content/scripts/manage_content.py +90 -0
- package/skills/adgine-geo-content/scripts/manage_jobs.py +184 -0
- package/skills/adgine-geo-dashboard/SKILL.md +181 -0
- package/skills/adgine-geo-dashboard/scripts/_client.py +341 -0
- package/skills/adgine-geo-dashboard/scripts/check_integrations.py +139 -0
- package/skills/adgine-geo-dashboard/scripts/get_overview.py +155 -0
- package/skills/adgine-geo-dashboard/scripts/get_visibility_overview.py +93 -0
- package/skills/adgine-geo-docs/SKILL.md +60 -0
- package/skills/adgine-geo-domains/SKILL.md +223 -0
- package/skills/adgine-geo-domains/scripts/_client.py +341 -0
- package/skills/adgine-geo-domains/scripts/get_domain.py +75 -0
- package/skills/adgine-geo-domains/scripts/list_domains.py +66 -0
- package/skills/adgine-geo-domains/scripts/search_domains.py +85 -0
- package/skills/adgine-geo-integrations/SKILL.md +188 -0
- package/skills/adgine-geo-integrations/scripts/_client.py +341 -0
- package/skills/adgine-geo-integrations/scripts/cloudflare_connect.py +155 -0
- package/skills/adgine-geo-integrations/scripts/cloudflare_worker.py +212 -0
- package/skills/adgine-geo-integrations/scripts/ga4_data.py +192 -0
- package/skills/adgine-geo-integrations/scripts/ga4_setup.py +120 -0
- package/skills/adgine-geo-opportunities/SKILL.md +162 -0
- package/skills/adgine-geo-opportunities/scripts/_client.py +341 -0
- package/skills/adgine-geo-opportunities/scripts/get_opportunity.py +90 -0
- package/skills/adgine-geo-opportunities/scripts/list_opportunities.py +92 -0
- package/skills/adgine-geo-performance/SKILL.md +177 -0
- package/skills/adgine-geo-performance/scripts/_client.py +341 -0
- package/skills/adgine-geo-performance/scripts/analyze_page.py +100 -0
- package/skills/adgine-geo-projects/SKILL.md +279 -0
- package/skills/adgine-geo-projects/scripts/_client.py +341 -0
- package/skills/adgine-geo-projects/scripts/check_auth.py +45 -0
- package/skills/adgine-geo-projects/scripts/list_projects.py +47 -0
- package/skills/adgine-geo-projects/scripts/manage_competitors.py +122 -0
- package/skills/adgine-geo-projects/scripts/manage_project.py +88 -0
- package/skills/adgine-geo-saas/SKILL.md +144 -0
- package/skills/adgine-geo-saas/scripts/_client.py +341 -0
- package/skills/adgine-geo-saas/scripts/check_domain.py +43 -0
- package/skills/adgine-geo-saas/scripts/create_website.py +56 -0
- package/skills/adgine-geo-saas/scripts/get_task.py +94 -0
- package/skills/adgine-geo-site-audit/README.md +281 -0
- package/skills/adgine-geo-site-audit/SKILL.md +449 -0
- package/skills/adgine-geo-site-audit/requirements.txt +5 -0
- package/skills/adgine-geo-site-audit/scripts/_client.py +341 -0
- package/skills/adgine-geo-site-audit/scripts/_geo_notice.py +55 -0
- package/skills/adgine-geo-site-audit/scripts/export_skill_package.py +197 -0
- package/skills/adgine-geo-site-audit/scripts/geo_collect.py +2443 -0
- package/skills/adgine-geo-site-audit/scripts/geo_score.py +490 -0
- package/skills/adgine-geo-site-audit/scripts/geo_timing.py +399 -0
- package/skills/adgine-geo-site-audit/scripts/geo_visibility.py +718 -0
- package/skills/adgine-geo-site-audit/scripts/render_report_pdf.py +1502 -0
- package/skills/adgine-geo-topics/SKILL.md +232 -0
- package/skills/adgine-geo-topics/scripts/_client.py +341 -0
- package/skills/adgine-geo-topics/scripts/generate_prompts.py +140 -0
- package/skills/adgine-geo-topics/scripts/manage_prompts.py +153 -0
- package/skills/adgine-geo-topics/scripts/manage_topics.py +145 -0
- package/skills/adgine-geo-visibility/SKILL.md +168 -0
- package/skills/adgine-geo-visibility/scripts/_client.py +341 -0
- package/skills/adgine-geo-visibility/scripts/get_execution.py +151 -0
- package/skills/adgine-geo-visibility/scripts/get_matrix.py +111 -0
- package/skills/adgine-geo-visibility/scripts/get_prompt_metrics.py +104 -0
- package/skills/adgine-geo-visibility/scripts/get_topic_metrics.py +201 -0
- package/skills/adgine-geo-visibility/scripts/get_visibility.py +166 -0
- package/skills/adgine-geo-wordpress/SKILL.md +151 -0
- package/skills/adgine-geo-wordpress/scripts/_client.py +341 -0
- package/skills/adgine-geo-wordpress/scripts/list_publishable.py +62 -0
- package/skills/adgine-geo-wordpress/scripts/manage_credentials.py +139 -0
- package/skills/adgine-geo-wordpress/scripts/manage_publishes.py +108 -0
- package/skills/adgine-geo-wordpress/scripts/publish.py +79 -0
- package/skills/scripts/check_version.py +290 -0
- package/skills/setup.py +248 -0
- package/src/config.js +126 -0
- package/src/doctor.js +137 -0
- package/src/install.js +90 -0
- package/src/manifest.js +99 -0
- package/src/runner.js +159 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Summarize GEO audit timings without slowing down the normal workflow.
|
|
3
|
+
|
|
4
|
+
Normal audits should use the `artifacts` command once at the end. It reads the
|
|
5
|
+
timings already written by the collector/scorer/visibility/PDF scripts and does
|
|
6
|
+
not add start/stop marker overhead to the workflow.
|
|
7
|
+
|
|
8
|
+
The legacy `start`/`mark` ledger commands are kept for deep debugging only.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import time
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Optional
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _now() -> tuple[float, str]:
|
|
21
|
+
timestamp = time.time()
|
|
22
|
+
iso = datetime.fromtimestamp(timestamp, timezone.utc).isoformat()
|
|
23
|
+
return timestamp, iso
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _read_json(path: Path) -> dict[str, Any]:
|
|
27
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _write_json(path: Path, data: dict[str, Any]) -> None:
|
|
31
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create_ledger(path: Path, *, label: str = "", metadata: Optional[dict[str, Any]] = None) -> dict[str, Any]:
|
|
36
|
+
timestamp, iso = _now()
|
|
37
|
+
ledger = {
|
|
38
|
+
"label": label,
|
|
39
|
+
"metadata": metadata or {},
|
|
40
|
+
"started_at": iso,
|
|
41
|
+
"events": [
|
|
42
|
+
{"name": "workflow_start", "timestamp": timestamp, "iso": iso, "metadata": {}}
|
|
43
|
+
],
|
|
44
|
+
"attachments": {},
|
|
45
|
+
}
|
|
46
|
+
_write_json(path, ledger)
|
|
47
|
+
return ledger
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def mark_event(path: Path, name: str, *, metadata: Optional[dict[str, Any]] = None) -> dict[str, Any]:
|
|
51
|
+
ledger = _read_json(path)
|
|
52
|
+
timestamp, iso = _now()
|
|
53
|
+
ledger.setdefault("events", []).append({
|
|
54
|
+
"name": name,
|
|
55
|
+
"timestamp": timestamp,
|
|
56
|
+
"iso": iso,
|
|
57
|
+
"metadata": metadata or {},
|
|
58
|
+
})
|
|
59
|
+
_write_json(path, ledger)
|
|
60
|
+
return ledger
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _extract_timings(payload: dict[str, Any]) -> dict[str, Any]:
|
|
64
|
+
meta = payload.get("meta")
|
|
65
|
+
if isinstance(meta, dict) and isinstance(meta.get("timings"), dict):
|
|
66
|
+
return {
|
|
67
|
+
"source": "meta.timings",
|
|
68
|
+
"timings": meta["timings"],
|
|
69
|
+
"total_seconds": meta.get("report_generation_elapsed_seconds") or meta["timings"].get("total_collection_seconds"),
|
|
70
|
+
}
|
|
71
|
+
if isinstance(payload.get("timings"), dict):
|
|
72
|
+
return {
|
|
73
|
+
"source": "timings",
|
|
74
|
+
"timings": payload["timings"],
|
|
75
|
+
"total_seconds": _first_total(payload["timings"]),
|
|
76
|
+
}
|
|
77
|
+
return {"source": "", "timings": {}, "total_seconds": None}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _first_total(timings: dict[str, Any]) -> Optional[float]:
|
|
81
|
+
for key in (
|
|
82
|
+
"total_collection_seconds",
|
|
83
|
+
"total_score_module_seconds",
|
|
84
|
+
"total_visibility_prepare_seconds",
|
|
85
|
+
"total_visibility_score_seconds",
|
|
86
|
+
"total_pdf_module_seconds",
|
|
87
|
+
):
|
|
88
|
+
if key in timings:
|
|
89
|
+
try:
|
|
90
|
+
return float(timings[key])
|
|
91
|
+
except (TypeError, ValueError):
|
|
92
|
+
return None
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def attach_json(path: Path, name: str, json_path: Path) -> dict[str, Any]:
|
|
97
|
+
ledger = _read_json(path)
|
|
98
|
+
payload = _read_json(json_path)
|
|
99
|
+
attachment = {
|
|
100
|
+
"path": str(json_path.expanduser().absolute()),
|
|
101
|
+
}
|
|
102
|
+
attachment.update(_extract_timings(payload))
|
|
103
|
+
ledger.setdefault("attachments", {})[name] = attachment
|
|
104
|
+
_write_json(path, ledger)
|
|
105
|
+
return ledger
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def artifact_attachment(name: str, json_path: Optional[Path]) -> Optional[dict[str, Any]]:
|
|
109
|
+
if not json_path:
|
|
110
|
+
return None
|
|
111
|
+
path = json_path.expanduser().absolute()
|
|
112
|
+
if not path.exists():
|
|
113
|
+
return {
|
|
114
|
+
"path": str(path),
|
|
115
|
+
"missing": True,
|
|
116
|
+
"source": "",
|
|
117
|
+
"timings": {},
|
|
118
|
+
"total_seconds": None,
|
|
119
|
+
}
|
|
120
|
+
payload = _read_json(path)
|
|
121
|
+
attachment = {
|
|
122
|
+
"path": str(path),
|
|
123
|
+
"missing": False,
|
|
124
|
+
"modified_at": datetime.fromtimestamp(path.stat().st_mtime, timezone.utc).isoformat(),
|
|
125
|
+
}
|
|
126
|
+
attachment.update(_extract_timings(payload))
|
|
127
|
+
return attachment
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def summarize_artifacts(
|
|
131
|
+
*,
|
|
132
|
+
label: str = "",
|
|
133
|
+
collect_json: Optional[Path] = None,
|
|
134
|
+
score_json: Optional[Path] = None,
|
|
135
|
+
visibility_plan_json: Optional[Path] = None,
|
|
136
|
+
visibility_results_json: Optional[Path] = None,
|
|
137
|
+
pdf_timing_json: Optional[Path] = None,
|
|
138
|
+
ui_elapsed_seconds: Optional[float] = None,
|
|
139
|
+
) -> dict[str, Any]:
|
|
140
|
+
attachments: dict[str, Any] = {}
|
|
141
|
+
for name, path in (
|
|
142
|
+
("collect", collect_json),
|
|
143
|
+
("score", score_json),
|
|
144
|
+
("visibility_prepare", visibility_plan_json),
|
|
145
|
+
("visibility_score", visibility_results_json),
|
|
146
|
+
("pdf", pdf_timing_json),
|
|
147
|
+
):
|
|
148
|
+
attachment = artifact_attachment(name, path)
|
|
149
|
+
if attachment:
|
|
150
|
+
attachments[name] = attachment
|
|
151
|
+
|
|
152
|
+
known_script_seconds = round(
|
|
153
|
+
sum(
|
|
154
|
+
float(item["total_seconds"])
|
|
155
|
+
for item in attachments.values()
|
|
156
|
+
if isinstance(item.get("total_seconds"), (int, float))
|
|
157
|
+
),
|
|
158
|
+
3,
|
|
159
|
+
)
|
|
160
|
+
untracked_agent_overhead_seconds = None
|
|
161
|
+
if ui_elapsed_seconds is not None:
|
|
162
|
+
untracked_agent_overhead_seconds = round(float(ui_elapsed_seconds) - known_script_seconds, 3)
|
|
163
|
+
|
|
164
|
+
artifact_timeline = sorted(
|
|
165
|
+
[
|
|
166
|
+
{
|
|
167
|
+
"name": name,
|
|
168
|
+
"modified_at": attachment["modified_at"],
|
|
169
|
+
"path": attachment["path"],
|
|
170
|
+
}
|
|
171
|
+
for name, attachment in attachments.items()
|
|
172
|
+
if attachment.get("modified_at")
|
|
173
|
+
],
|
|
174
|
+
key=lambda item: item["modified_at"],
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
"label": label,
|
|
179
|
+
"measurement_mode": "artifact_summary",
|
|
180
|
+
"note": (
|
|
181
|
+
"Low-overhead timing summary. It reads existing artifact timings once at the end. "
|
|
182
|
+
"It does not measure full UI/agent elapsed time unless ui_elapsed_seconds is provided."
|
|
183
|
+
),
|
|
184
|
+
"total_workflow_seconds": ui_elapsed_seconds,
|
|
185
|
+
"known_script_seconds": known_script_seconds,
|
|
186
|
+
"untracked_agent_overhead_seconds": untracked_agent_overhead_seconds,
|
|
187
|
+
"artifact_timeline": artifact_timeline,
|
|
188
|
+
"attachments": attachments,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def summarize_ledger(ledger: dict[str, Any]) -> dict[str, Any]:
|
|
193
|
+
events = ledger.get("events", [])
|
|
194
|
+
spans: list[dict[str, Any]] = []
|
|
195
|
+
for previous, current in zip(events, events[1:]):
|
|
196
|
+
elapsed = float(current["timestamp"]) - float(previous["timestamp"])
|
|
197
|
+
spans.append({
|
|
198
|
+
"from": previous["name"],
|
|
199
|
+
"to": current["name"],
|
|
200
|
+
"seconds": round(elapsed, 3),
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
named_starts: dict[str, dict[str, Any]] = {}
|
|
204
|
+
named_spans: list[dict[str, Any]] = []
|
|
205
|
+
for event in events:
|
|
206
|
+
name = str(event.get("name", ""))
|
|
207
|
+
if name.endswith("_start"):
|
|
208
|
+
named_starts[name[:-6]] = event
|
|
209
|
+
elif name.endswith("_end"):
|
|
210
|
+
key = name[:-4]
|
|
211
|
+
start = named_starts.get(key)
|
|
212
|
+
if start:
|
|
213
|
+
named_spans.append({
|
|
214
|
+
"name": key,
|
|
215
|
+
"seconds": round(float(event["timestamp"]) - float(start["timestamp"]), 3),
|
|
216
|
+
"start": start.get("iso", ""),
|
|
217
|
+
"end": event.get("iso", ""),
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
total_seconds = None
|
|
221
|
+
if events:
|
|
222
|
+
total_seconds = round(float(events[-1]["timestamp"]) - float(events[0]["timestamp"]), 3)
|
|
223
|
+
|
|
224
|
+
attachments = ledger.get("attachments", {})
|
|
225
|
+
known_script_seconds = round(
|
|
226
|
+
sum(
|
|
227
|
+
float(item["total_seconds"])
|
|
228
|
+
for item in attachments.values()
|
|
229
|
+
if isinstance(item.get("total_seconds"), (int, float))
|
|
230
|
+
),
|
|
231
|
+
3,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
"label": ledger.get("label", ""),
|
|
236
|
+
"metadata": ledger.get("metadata", {}),
|
|
237
|
+
"started_at": ledger.get("started_at", ""),
|
|
238
|
+
"total_workflow_seconds": total_seconds,
|
|
239
|
+
"known_script_seconds": known_script_seconds,
|
|
240
|
+
"named_spans": named_spans,
|
|
241
|
+
"event_spans": spans,
|
|
242
|
+
"attachments": attachments,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def format_markdown_summary(summary: dict[str, Any]) -> str:
|
|
247
|
+
if summary.get("measurement_mode") == "artifact_summary":
|
|
248
|
+
lines = [
|
|
249
|
+
f"# GEO Timing Debug: {summary.get('label') or 'workflow'}",
|
|
250
|
+
"",
|
|
251
|
+
summary.get("note", ""),
|
|
252
|
+
"",
|
|
253
|
+
f"**Workflow total**: {summary.get('total_workflow_seconds') if summary.get('total_workflow_seconds') is not None else 'not measured'}",
|
|
254
|
+
f"**Known script-internal seconds**: {summary.get('known_script_seconds', '')}s",
|
|
255
|
+
]
|
|
256
|
+
if summary.get("untracked_agent_overhead_seconds") is not None:
|
|
257
|
+
lines.append(f"**Untracked agent/UI overhead**: {summary['untracked_agent_overhead_seconds']}s")
|
|
258
|
+
|
|
259
|
+
timeline = summary.get("artifact_timeline", [])
|
|
260
|
+
if timeline:
|
|
261
|
+
lines.extend(["", "## Artifact Timeline", "", "| Artifact | Modified At | Path |", "|---|---|---|"])
|
|
262
|
+
for item in timeline:
|
|
263
|
+
lines.append(f"| {item['name']} | {item['modified_at']} | `{item['path']}` |")
|
|
264
|
+
|
|
265
|
+
attachments = summary.get("attachments", {})
|
|
266
|
+
if attachments:
|
|
267
|
+
lines.extend([
|
|
268
|
+
"",
|
|
269
|
+
"## Script-Internal Timings",
|
|
270
|
+
"",
|
|
271
|
+
"These values come from each script's own timing block. They are not end-to-end phase durations.",
|
|
272
|
+
"",
|
|
273
|
+
"| Attachment | Source | Internal Total | Path |",
|
|
274
|
+
"|---|---|---:|---|",
|
|
275
|
+
])
|
|
276
|
+
for name, attachment in attachments.items():
|
|
277
|
+
total = attachment.get("total_seconds")
|
|
278
|
+
total_text = f"{float(total):.3f}s" if isinstance(total, (int, float)) else ""
|
|
279
|
+
lines.append(
|
|
280
|
+
f"| {name} | {attachment.get('source', '')} | {total_text} | `{attachment.get('path', '')}` |"
|
|
281
|
+
)
|
|
282
|
+
return "\n".join(lines) + "\n"
|
|
283
|
+
|
|
284
|
+
lines = [
|
|
285
|
+
f"# GEO Timing Debug: {summary.get('label') or 'workflow'}",
|
|
286
|
+
"",
|
|
287
|
+
f"**Started at**: {summary.get('started_at', '')}",
|
|
288
|
+
f"**Workflow total**: {summary.get('total_workflow_seconds')}s",
|
|
289
|
+
f"**Known script seconds**: {summary.get('known_script_seconds', '')}s",
|
|
290
|
+
"",
|
|
291
|
+
"## Named Spans",
|
|
292
|
+
"",
|
|
293
|
+
"| Step | Seconds |",
|
|
294
|
+
"|---|---:|",
|
|
295
|
+
]
|
|
296
|
+
for span in summary.get("named_spans", []):
|
|
297
|
+
lines.append(f"| {span['name']} | {span['seconds']:.3f}s |")
|
|
298
|
+
|
|
299
|
+
attachments = summary.get("attachments", {})
|
|
300
|
+
if attachments:
|
|
301
|
+
lines.extend(["", "## Attached Script Timings", "", "| Attachment | Source | Total | Path |", "|---|---|---:|---|"])
|
|
302
|
+
for name, attachment in attachments.items():
|
|
303
|
+
total = attachment.get("total_seconds")
|
|
304
|
+
total_text = f"{float(total):.3f}s" if isinstance(total, (int, float)) else ""
|
|
305
|
+
lines.append(
|
|
306
|
+
f"| {name} | {attachment.get('source', '')} | {total_text} | `{attachment.get('path', '')}` |"
|
|
307
|
+
)
|
|
308
|
+
return "\n".join(lines) + "\n"
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def main(argv: Optional[list[str]] = None) -> int:
|
|
312
|
+
parser = argparse.ArgumentParser(description="Track and summarize GEO audit workflow timings.")
|
|
313
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
314
|
+
|
|
315
|
+
start = subparsers.add_parser("start", help="Create a new timing ledger")
|
|
316
|
+
start.add_argument("ledger_json")
|
|
317
|
+
start.add_argument("--label", default="")
|
|
318
|
+
start.add_argument("--metadata-json", default="{}")
|
|
319
|
+
|
|
320
|
+
mark = subparsers.add_parser("mark", help="Append a timing event")
|
|
321
|
+
mark.add_argument("ledger_json")
|
|
322
|
+
mark.add_argument("name")
|
|
323
|
+
mark.add_argument("--metadata-json", default="{}")
|
|
324
|
+
|
|
325
|
+
attach = subparsers.add_parser("attach", help="Attach a JSON artifact and extract its timings")
|
|
326
|
+
attach.add_argument("ledger_json")
|
|
327
|
+
attach.add_argument("name")
|
|
328
|
+
attach.add_argument("json_path")
|
|
329
|
+
|
|
330
|
+
summary = subparsers.add_parser("summary", help="Summarize a timing ledger")
|
|
331
|
+
summary.add_argument("ledger_json")
|
|
332
|
+
summary.add_argument("--output", "-o")
|
|
333
|
+
summary.add_argument("--report")
|
|
334
|
+
|
|
335
|
+
artifacts = subparsers.add_parser("artifacts", help="Build a low-overhead timing summary from existing artifact JSON files")
|
|
336
|
+
artifacts.add_argument("--label", default="")
|
|
337
|
+
artifacts.add_argument("--collect-json")
|
|
338
|
+
artifacts.add_argument("--score-json")
|
|
339
|
+
artifacts.add_argument("--visibility-plan-json")
|
|
340
|
+
artifacts.add_argument("--visibility-results-json")
|
|
341
|
+
artifacts.add_argument("--pdf-timing-json")
|
|
342
|
+
artifacts.add_argument("--ui-elapsed-seconds", type=float, help="Optional UI-reported elapsed seconds, if available")
|
|
343
|
+
artifacts.add_argument("--output", "-o")
|
|
344
|
+
artifacts.add_argument("--report")
|
|
345
|
+
|
|
346
|
+
args = parser.parse_args(argv)
|
|
347
|
+
|
|
348
|
+
if args.command == "start":
|
|
349
|
+
metadata = json.loads(args.metadata_json)
|
|
350
|
+
create_ledger(Path(args.ledger_json), label=args.label, metadata=metadata)
|
|
351
|
+
return 0
|
|
352
|
+
|
|
353
|
+
if args.command == "mark":
|
|
354
|
+
metadata = json.loads(args.metadata_json)
|
|
355
|
+
mark_event(Path(args.ledger_json), args.name, metadata=metadata)
|
|
356
|
+
return 0
|
|
357
|
+
|
|
358
|
+
if args.command == "attach":
|
|
359
|
+
attach_json(Path(args.ledger_json), args.name, Path(args.json_path))
|
|
360
|
+
return 0
|
|
361
|
+
|
|
362
|
+
if args.command == "summary":
|
|
363
|
+
result = summarize_ledger(_read_json(Path(args.ledger_json)))
|
|
364
|
+
if args.output:
|
|
365
|
+
_write_json(Path(args.output), result)
|
|
366
|
+
else:
|
|
367
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
368
|
+
if args.report:
|
|
369
|
+
Path(args.report).expanduser().absolute().write_text(format_markdown_summary(result), encoding="utf-8")
|
|
370
|
+
return 0
|
|
371
|
+
|
|
372
|
+
if args.command == "artifacts":
|
|
373
|
+
result = summarize_artifacts(
|
|
374
|
+
label=args.label,
|
|
375
|
+
collect_json=Path(args.collect_json) if args.collect_json else None,
|
|
376
|
+
score_json=Path(args.score_json) if args.score_json else None,
|
|
377
|
+
visibility_plan_json=Path(args.visibility_plan_json) if args.visibility_plan_json else None,
|
|
378
|
+
visibility_results_json=Path(args.visibility_results_json) if args.visibility_results_json else None,
|
|
379
|
+
pdf_timing_json=Path(args.pdf_timing_json) if args.pdf_timing_json else None,
|
|
380
|
+
ui_elapsed_seconds=args.ui_elapsed_seconds,
|
|
381
|
+
)
|
|
382
|
+
if args.output:
|
|
383
|
+
_write_json(Path(args.output), result)
|
|
384
|
+
else:
|
|
385
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
386
|
+
if args.report:
|
|
387
|
+
Path(args.report).expanduser().absolute().write_text(format_markdown_summary(result), encoding="utf-8")
|
|
388
|
+
return 0
|
|
389
|
+
|
|
390
|
+
return 1
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
if __name__ == "__main__":
|
|
394
|
+
try:
|
|
395
|
+
import _geo_notice
|
|
396
|
+
_geo_notice.emit()
|
|
397
|
+
except Exception:
|
|
398
|
+
pass
|
|
399
|
+
raise SystemExit(main())
|