@robot-inventor/agent-skills 0.0.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +31 -31
- package/package.json +34 -33
- package/plugin.json +11 -11
- package/scripts/codex.ts +35 -0
- package/scripts/opencode.ts +26 -26
- package/scripts/skills.ts +6 -0
- package/scripts/syncVersion.ts +10 -0
- package/skills/cleanup-changes/SKILL.md +33 -33
- package/skills/identify-search-maintenance/SKILL.md +78 -78
- package/skills/identify-search-maintenance/references/selection-rubric.md +45 -45
- package/skills/identify-search-maintenance/scripts/select_rank_led_candidates.py +212 -212
- package/skills/prove-it-diagnostics/SKILL.md +60 -60
- package/skills/review-loop/SKILL.md +45 -45
- package/skills/simple-engineering/SKILL.md +27 -27
- package/skills/web-master/SKILL.md +109 -109
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
# Rank-led candidate selection rubric
|
|
2
|
-
|
|
3
|
-
## Normalize the export
|
|
4
|
-
|
|
5
|
-
Create a CSV with this exact header before using the script:
|
|
6
|
-
|
|
7
|
-
```csv
|
|
8
|
-
url,current_clicks,previous_clicks,current_impressions,previous_impressions,current_position,previous_position
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
`current_*` is the recent period and `previous_*` is its comparison period. Preserve the original Search Console exports as evidence. Do not combine GA4 and Search Console counts in the same columns. The normalized page CSV is only a ranking-loss screen; it cannot establish search demand.
|
|
12
|
-
|
|
13
|
-
For annual comparison, make a second normalized CSV where `previous_*` is the equivalent period one year earlier. Run the screen on both CSVs, union their candidates, and show which comparison surfaced each URL. If that page did not exist then or the data is unavailable, report `N/A`.
|
|
14
|
-
|
|
15
|
-
## Default first-pass thresholds
|
|
16
|
-
|
|
17
|
-
The bundled script selects a page only if all conditions hold:
|
|
18
|
-
|
|
19
|
-
| Signal | Default | Reason |
|
|
20
|
-
| --------------------------------------- | ----------------------------: | ---------------------------------------- |
|
|
21
|
-
| Previous-period clicks | at least 100 | Avoid low-volume noise |
|
|
22
|
-
| Click change | -20% or worse | Requires a material loss |
|
|
23
|
-
| Impression change | within ±25% | Excludes most demand-led losses |
|
|
24
|
-
| Position change | +0.5 or worse | Requires a measurable rank deterioration |
|
|
25
|
-
| Click decline beyond impression decline | at least 10 percentage points | Requires loss beyond demand movement |
|
|
26
|
-
|
|
27
|
-
These are screening defaults, not proof. A page can be excluded despite passing them when an independent demand proxy shows demand loss, a one-off event occurred, a competing URL gained the query, or the task is obsolete. A page can be included with explained threshold adjustments only when the report documents why.
|
|
28
|
-
|
|
29
|
-
## Interpret the metrics
|
|
30
|
-
|
|
31
|
-
- Position delta is `current_position - previous_position`; a positive number is worse.
|
|
32
|
-
- Page-level impressions are not demand: a lower rank can itself lower the page's impressions. Property-wide query impressions can also fall when the whole site loses visibility. Use Google Trends, Keyword Planner, or another documented independent source to assess demand. Compare property-wide (unfiltered-by-page) query impressions only as a corroborating visibility signal, then compare the target URL's rank/clicks separately.
|
|
33
|
-
- A stable page-level impression total can hide query-mix movement. Verify individual declined queries before calling the loss rank-led, and inspect pages that gained the query for cannibalization.
|
|
34
|
-
- A stable position with a falling CTR is a CTR/SERP case, not a rank-led case.
|
|
35
|
-
- A fall in impressions paired with a similar fall in clicks is normally demand-led or obsolescence-led until evidence proves otherwise.
|
|
36
|
-
- Search Console average position is an aggregate. Use live Google searches only as a point-in-time confirmation and label locale, device, signed-in state, and date.
|
|
37
|
-
|
|
38
|
-
## Completion checklist
|
|
39
|
-
|
|
40
|
-
- [ ] Recent and year-over-year comparisons were both attempted.
|
|
41
|
-
- [ ] Every final candidate has page-level metrics, at least three declined historic queries, and property-wide visibility checks for those queries.
|
|
42
|
-
- [ ] Every final candidate has independent demand evidence, a URL Inspection/index/canonical/availability health check, and no confirmed cannibalization.
|
|
43
|
-
- [ ] Google SERP checks list query, target visibility, competitors, and material SERP features.
|
|
44
|
-
- [ ] The report explicitly separates rank-led candidates from demand-led, CTR-only, technical, and insufficient-evidence cases.
|
|
45
|
-
- [ ] The report contains diagnoses and maintenance priorities, but no article edits.
|
|
1
|
+
# Rank-led candidate selection rubric
|
|
2
|
+
|
|
3
|
+
## Normalize the export
|
|
4
|
+
|
|
5
|
+
Create a CSV with this exact header before using the script:
|
|
6
|
+
|
|
7
|
+
```csv
|
|
8
|
+
url,current_clicks,previous_clicks,current_impressions,previous_impressions,current_position,previous_position
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`current_*` is the recent period and `previous_*` is its comparison period. Preserve the original Search Console exports as evidence. Do not combine GA4 and Search Console counts in the same columns. The normalized page CSV is only a ranking-loss screen; it cannot establish search demand.
|
|
12
|
+
|
|
13
|
+
For annual comparison, make a second normalized CSV where `previous_*` is the equivalent period one year earlier. Run the screen on both CSVs, union their candidates, and show which comparison surfaced each URL. If that page did not exist then or the data is unavailable, report `N/A`.
|
|
14
|
+
|
|
15
|
+
## Default first-pass thresholds
|
|
16
|
+
|
|
17
|
+
The bundled script selects a page only if all conditions hold:
|
|
18
|
+
|
|
19
|
+
| Signal | Default | Reason |
|
|
20
|
+
| --------------------------------------- | ----------------------------: | ---------------------------------------- |
|
|
21
|
+
| Previous-period clicks | at least 100 | Avoid low-volume noise |
|
|
22
|
+
| Click change | -20% or worse | Requires a material loss |
|
|
23
|
+
| Impression change | within ±25% | Excludes most demand-led losses |
|
|
24
|
+
| Position change | +0.5 or worse | Requires a measurable rank deterioration |
|
|
25
|
+
| Click decline beyond impression decline | at least 10 percentage points | Requires loss beyond demand movement |
|
|
26
|
+
|
|
27
|
+
These are screening defaults, not proof. A page can be excluded despite passing them when an independent demand proxy shows demand loss, a one-off event occurred, a competing URL gained the query, or the task is obsolete. A page can be included with explained threshold adjustments only when the report documents why.
|
|
28
|
+
|
|
29
|
+
## Interpret the metrics
|
|
30
|
+
|
|
31
|
+
- Position delta is `current_position - previous_position`; a positive number is worse.
|
|
32
|
+
- Page-level impressions are not demand: a lower rank can itself lower the page's impressions. Property-wide query impressions can also fall when the whole site loses visibility. Use Google Trends, Keyword Planner, or another documented independent source to assess demand. Compare property-wide (unfiltered-by-page) query impressions only as a corroborating visibility signal, then compare the target URL's rank/clicks separately.
|
|
33
|
+
- A stable page-level impression total can hide query-mix movement. Verify individual declined queries before calling the loss rank-led, and inspect pages that gained the query for cannibalization.
|
|
34
|
+
- A stable position with a falling CTR is a CTR/SERP case, not a rank-led case.
|
|
35
|
+
- A fall in impressions paired with a similar fall in clicks is normally demand-led or obsolescence-led until evidence proves otherwise.
|
|
36
|
+
- Search Console average position is an aggregate. Use live Google searches only as a point-in-time confirmation and label locale, device, signed-in state, and date.
|
|
37
|
+
|
|
38
|
+
## Completion checklist
|
|
39
|
+
|
|
40
|
+
- [ ] Recent and year-over-year comparisons were both attempted.
|
|
41
|
+
- [ ] Every final candidate has page-level metrics, at least three declined historic queries, and property-wide visibility checks for those queries.
|
|
42
|
+
- [ ] Every final candidate has independent demand evidence, a URL Inspection/index/canonical/availability health check, and no confirmed cannibalization.
|
|
43
|
+
- [ ] Google SERP checks list query, target visibility, competitors, and material SERP features.
|
|
44
|
+
- [ ] The report explicitly separates rank-led candidates from demand-led, CTR-only, technical, and insufficient-evidence cases.
|
|
45
|
+
- [ ] The report contains diagnoses and maintenance priorities, but no article edits.
|
|
@@ -1,212 +1,212 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Screen normalized Search Console page data for rank-led traffic-loss candidates."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import argparse
|
|
7
|
-
import csv
|
|
8
|
-
import math
|
|
9
|
-
import sys
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from typing import Iterable, TextIO
|
|
12
|
-
|
|
13
|
-
REQUIRED_COLUMNS = (
|
|
14
|
-
"url",
|
|
15
|
-
"current_clicks",
|
|
16
|
-
"previous_clicks",
|
|
17
|
-
"current_impressions",
|
|
18
|
-
"previous_impressions",
|
|
19
|
-
"current_position",
|
|
20
|
-
"previous_position",
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def parse_number(value: str, column: str, row_number: int) -> float:
|
|
25
|
-
try:
|
|
26
|
-
number = float(value.replace(",", "").strip())
|
|
27
|
-
except (AttributeError, ValueError) as error:
|
|
28
|
-
raise ValueError(
|
|
29
|
-
f"Row {row_number}: {column} must be numeric, got {value!r}."
|
|
30
|
-
) from error
|
|
31
|
-
if not math.isfinite(number):
|
|
32
|
-
raise ValueError(f"Row {row_number}: {column} must be finite, got {value!r}.")
|
|
33
|
-
if (column.endswith("clicks") or column.endswith("impressions")) and number < 0:
|
|
34
|
-
raise ValueError(
|
|
35
|
-
f"Row {row_number}: {column} cannot be negative, got {value!r}."
|
|
36
|
-
)
|
|
37
|
-
if column.endswith("position") and number <= 0:
|
|
38
|
-
raise ValueError(
|
|
39
|
-
f"Row {row_number}: {column} must be greater than zero, got {value!r}."
|
|
40
|
-
)
|
|
41
|
-
return number
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def percentage_change(current: float, previous: float) -> float:
|
|
45
|
-
return (current - previous) / previous
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def read_rows(stream: TextIO) -> Iterable[dict[str, str]]:
|
|
49
|
-
reader = csv.DictReader(stream)
|
|
50
|
-
if reader.fieldnames is None:
|
|
51
|
-
raise ValueError("Input CSV has no header row.")
|
|
52
|
-
|
|
53
|
-
reader.fieldnames[0] = reader.fieldnames[0].lstrip("\ufeff")
|
|
54
|
-
missing = [column for column in REQUIRED_COLUMNS if column not in reader.fieldnames]
|
|
55
|
-
if missing:
|
|
56
|
-
raise ValueError(
|
|
57
|
-
f"Input CSV is missing required columns: {', '.join(missing)}."
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
return reader
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def select_candidates(
|
|
64
|
-
rows: Iterable[dict[str, str]],
|
|
65
|
-
min_previous_clicks: float,
|
|
66
|
-
max_click_change: float,
|
|
67
|
-
max_absolute_impression_change: float,
|
|
68
|
-
min_position_worsening: float,
|
|
69
|
-
min_click_gap_vs_impressions: float,
|
|
70
|
-
) -> list[dict[str, float | str]]:
|
|
71
|
-
candidates: list[dict[str, float | str]] = []
|
|
72
|
-
seen_urls: set[str] = set()
|
|
73
|
-
for row_number, row in enumerate(rows, start=2):
|
|
74
|
-
url = row["url"].strip()
|
|
75
|
-
if not url:
|
|
76
|
-
raise ValueError(f"Row {row_number}: url cannot be empty.")
|
|
77
|
-
if url in seen_urls:
|
|
78
|
-
raise ValueError(
|
|
79
|
-
f"Row {row_number}: duplicate url {url!r}; aggregate the export first."
|
|
80
|
-
)
|
|
81
|
-
seen_urls.add(url)
|
|
82
|
-
values = {
|
|
83
|
-
column: parse_number(row[column], column, row_number)
|
|
84
|
-
for column in REQUIRED_COLUMNS[1:]
|
|
85
|
-
}
|
|
86
|
-
previous_clicks = values["previous_clicks"]
|
|
87
|
-
previous_impressions = values["previous_impressions"]
|
|
88
|
-
if previous_clicks <= 0 or previous_impressions <= 0:
|
|
89
|
-
continue
|
|
90
|
-
|
|
91
|
-
click_change = percentage_change(values["current_clicks"], previous_clicks)
|
|
92
|
-
impression_change = percentage_change(
|
|
93
|
-
values["current_impressions"], previous_impressions
|
|
94
|
-
)
|
|
95
|
-
position_worsening = values["current_position"] - values["previous_position"]
|
|
96
|
-
click_gap_vs_impressions = impression_change - click_change
|
|
97
|
-
|
|
98
|
-
if (
|
|
99
|
-
previous_clicks >= min_previous_clicks
|
|
100
|
-
and click_change <= max_click_change
|
|
101
|
-
and abs(impression_change) <= max_absolute_impression_change
|
|
102
|
-
and position_worsening >= min_position_worsening
|
|
103
|
-
and click_gap_vs_impressions >= min_click_gap_vs_impressions
|
|
104
|
-
):
|
|
105
|
-
candidates.append(
|
|
106
|
-
{
|
|
107
|
-
"url": url,
|
|
108
|
-
"current_clicks": values["current_clicks"],
|
|
109
|
-
"previous_clicks": previous_clicks,
|
|
110
|
-
"click_change": click_change,
|
|
111
|
-
"current_impressions": values["current_impressions"],
|
|
112
|
-
"previous_impressions": previous_impressions,
|
|
113
|
-
"impression_change": impression_change,
|
|
114
|
-
"current_position": values["current_position"],
|
|
115
|
-
"previous_position": values["previous_position"],
|
|
116
|
-
"position_worsening": position_worsening,
|
|
117
|
-
"click_gap_vs_impressions": click_gap_vs_impressions,
|
|
118
|
-
}
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
return sorted(
|
|
122
|
-
candidates,
|
|
123
|
-
key=lambda candidate: (
|
|
124
|
-
float(candidate["previous_clicks"]) - float(candidate["current_clicks"])
|
|
125
|
-
),
|
|
126
|
-
reverse=True,
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def percentage(value: float) -> str:
|
|
131
|
-
return f"{value * 100:.1f}%"
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def markdown(candidates: Iterable[dict[str, float | str]]) -> str:
|
|
135
|
-
lines = [
|
|
136
|
-
"| URL | Clicks (current / previous) | Click change | Impressions change | Position worsening |",
|
|
137
|
-
"| --- | ---: | ---: | ---: | ---: |",
|
|
138
|
-
]
|
|
139
|
-
for candidate in candidates:
|
|
140
|
-
lines.append(
|
|
141
|
-
"| {url} | {current_clicks:.0f} / {previous_clicks:.0f} | {click_change} | {impression_change} | {position_worsening:+.1f} |".format(
|
|
142
|
-
url=candidate["url"],
|
|
143
|
-
current_clicks=float(candidate["current_clicks"]),
|
|
144
|
-
previous_clicks=float(candidate["previous_clicks"]),
|
|
145
|
-
click_change=percentage(float(candidate["click_change"])),
|
|
146
|
-
impression_change=percentage(float(candidate["impression_change"])),
|
|
147
|
-
position_worsening=float(candidate["position_worsening"]),
|
|
148
|
-
)
|
|
149
|
-
)
|
|
150
|
-
return "\n".join(lines)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def main() -> int:
|
|
154
|
-
parser = argparse.ArgumentParser(description=__doc__)
|
|
155
|
-
parser.add_argument(
|
|
156
|
-
"input", help="Normalized CSV path, or - to read CSV from standard input."
|
|
157
|
-
)
|
|
158
|
-
parser.add_argument("--min-previous-clicks", type=float, default=100)
|
|
159
|
-
parser.add_argument("--max-click-change", type=float, default=-0.20)
|
|
160
|
-
parser.add_argument("--max-absolute-impression-change", type=float, default=0.25)
|
|
161
|
-
parser.add_argument("--min-position-worsening", type=float, default=0.5)
|
|
162
|
-
parser.add_argument("--min-click-gap-vs-impressions", type=float, default=0.10)
|
|
163
|
-
parser.add_argument("--format", choices=("markdown", "csv"), default="markdown")
|
|
164
|
-
arguments = parser.parse_args()
|
|
165
|
-
|
|
166
|
-
try:
|
|
167
|
-
if arguments.input == "-":
|
|
168
|
-
candidates = select_candidates(
|
|
169
|
-
read_rows(sys.stdin),
|
|
170
|
-
arguments.min_previous_clicks,
|
|
171
|
-
arguments.max_click_change,
|
|
172
|
-
arguments.max_absolute_impression_change,
|
|
173
|
-
arguments.min_position_worsening,
|
|
174
|
-
arguments.min_click_gap_vs_impressions,
|
|
175
|
-
)
|
|
176
|
-
else:
|
|
177
|
-
with Path(arguments.input).open(
|
|
178
|
-
encoding="utf-8-sig", newline=""
|
|
179
|
-
) as input_file:
|
|
180
|
-
candidates = select_candidates(
|
|
181
|
-
read_rows(input_file),
|
|
182
|
-
arguments.min_previous_clicks,
|
|
183
|
-
arguments.max_click_change,
|
|
184
|
-
arguments.max_absolute_impression_change,
|
|
185
|
-
arguments.min_position_worsening,
|
|
186
|
-
arguments.min_click_gap_vs_impressions,
|
|
187
|
-
)
|
|
188
|
-
except (OSError, ValueError, csv.Error) as error:
|
|
189
|
-
parser.error(str(error))
|
|
190
|
-
|
|
191
|
-
if arguments.format == "markdown":
|
|
192
|
-
print(markdown(candidates))
|
|
193
|
-
return 0
|
|
194
|
-
|
|
195
|
-
writer = csv.DictWriter(
|
|
196
|
-
sys.stdout,
|
|
197
|
-
fieldnames=(
|
|
198
|
-
"url",
|
|
199
|
-
*REQUIRED_COLUMNS[1:],
|
|
200
|
-
"click_change",
|
|
201
|
-
"impression_change",
|
|
202
|
-
"position_worsening",
|
|
203
|
-
"click_gap_vs_impressions",
|
|
204
|
-
),
|
|
205
|
-
)
|
|
206
|
-
writer.writeheader()
|
|
207
|
-
writer.writerows(candidates)
|
|
208
|
-
return 0
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if __name__ == "__main__":
|
|
212
|
-
raise SystemExit(main())
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Screen normalized Search Console page data for rank-led traffic-loss candidates."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import csv
|
|
8
|
+
import math
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Iterable, TextIO
|
|
12
|
+
|
|
13
|
+
REQUIRED_COLUMNS = (
|
|
14
|
+
"url",
|
|
15
|
+
"current_clicks",
|
|
16
|
+
"previous_clicks",
|
|
17
|
+
"current_impressions",
|
|
18
|
+
"previous_impressions",
|
|
19
|
+
"current_position",
|
|
20
|
+
"previous_position",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_number(value: str, column: str, row_number: int) -> float:
|
|
25
|
+
try:
|
|
26
|
+
number = float(value.replace(",", "").strip())
|
|
27
|
+
except (AttributeError, ValueError) as error:
|
|
28
|
+
raise ValueError(
|
|
29
|
+
f"Row {row_number}: {column} must be numeric, got {value!r}."
|
|
30
|
+
) from error
|
|
31
|
+
if not math.isfinite(number):
|
|
32
|
+
raise ValueError(f"Row {row_number}: {column} must be finite, got {value!r}.")
|
|
33
|
+
if (column.endswith("clicks") or column.endswith("impressions")) and number < 0:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"Row {row_number}: {column} cannot be negative, got {value!r}."
|
|
36
|
+
)
|
|
37
|
+
if column.endswith("position") and number <= 0:
|
|
38
|
+
raise ValueError(
|
|
39
|
+
f"Row {row_number}: {column} must be greater than zero, got {value!r}."
|
|
40
|
+
)
|
|
41
|
+
return number
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def percentage_change(current: float, previous: float) -> float:
|
|
45
|
+
return (current - previous) / previous
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def read_rows(stream: TextIO) -> Iterable[dict[str, str]]:
|
|
49
|
+
reader = csv.DictReader(stream)
|
|
50
|
+
if reader.fieldnames is None:
|
|
51
|
+
raise ValueError("Input CSV has no header row.")
|
|
52
|
+
|
|
53
|
+
reader.fieldnames[0] = reader.fieldnames[0].lstrip("\ufeff")
|
|
54
|
+
missing = [column for column in REQUIRED_COLUMNS if column not in reader.fieldnames]
|
|
55
|
+
if missing:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"Input CSV is missing required columns: {', '.join(missing)}."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return reader
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def select_candidates(
|
|
64
|
+
rows: Iterable[dict[str, str]],
|
|
65
|
+
min_previous_clicks: float,
|
|
66
|
+
max_click_change: float,
|
|
67
|
+
max_absolute_impression_change: float,
|
|
68
|
+
min_position_worsening: float,
|
|
69
|
+
min_click_gap_vs_impressions: float,
|
|
70
|
+
) -> list[dict[str, float | str]]:
|
|
71
|
+
candidates: list[dict[str, float | str]] = []
|
|
72
|
+
seen_urls: set[str] = set()
|
|
73
|
+
for row_number, row in enumerate(rows, start=2):
|
|
74
|
+
url = row["url"].strip()
|
|
75
|
+
if not url:
|
|
76
|
+
raise ValueError(f"Row {row_number}: url cannot be empty.")
|
|
77
|
+
if url in seen_urls:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"Row {row_number}: duplicate url {url!r}; aggregate the export first."
|
|
80
|
+
)
|
|
81
|
+
seen_urls.add(url)
|
|
82
|
+
values = {
|
|
83
|
+
column: parse_number(row[column], column, row_number)
|
|
84
|
+
for column in REQUIRED_COLUMNS[1:]
|
|
85
|
+
}
|
|
86
|
+
previous_clicks = values["previous_clicks"]
|
|
87
|
+
previous_impressions = values["previous_impressions"]
|
|
88
|
+
if previous_clicks <= 0 or previous_impressions <= 0:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
click_change = percentage_change(values["current_clicks"], previous_clicks)
|
|
92
|
+
impression_change = percentage_change(
|
|
93
|
+
values["current_impressions"], previous_impressions
|
|
94
|
+
)
|
|
95
|
+
position_worsening = values["current_position"] - values["previous_position"]
|
|
96
|
+
click_gap_vs_impressions = impression_change - click_change
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
previous_clicks >= min_previous_clicks
|
|
100
|
+
and click_change <= max_click_change
|
|
101
|
+
and abs(impression_change) <= max_absolute_impression_change
|
|
102
|
+
and position_worsening >= min_position_worsening
|
|
103
|
+
and click_gap_vs_impressions >= min_click_gap_vs_impressions
|
|
104
|
+
):
|
|
105
|
+
candidates.append(
|
|
106
|
+
{
|
|
107
|
+
"url": url,
|
|
108
|
+
"current_clicks": values["current_clicks"],
|
|
109
|
+
"previous_clicks": previous_clicks,
|
|
110
|
+
"click_change": click_change,
|
|
111
|
+
"current_impressions": values["current_impressions"],
|
|
112
|
+
"previous_impressions": previous_impressions,
|
|
113
|
+
"impression_change": impression_change,
|
|
114
|
+
"current_position": values["current_position"],
|
|
115
|
+
"previous_position": values["previous_position"],
|
|
116
|
+
"position_worsening": position_worsening,
|
|
117
|
+
"click_gap_vs_impressions": click_gap_vs_impressions,
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return sorted(
|
|
122
|
+
candidates,
|
|
123
|
+
key=lambda candidate: (
|
|
124
|
+
float(candidate["previous_clicks"]) - float(candidate["current_clicks"])
|
|
125
|
+
),
|
|
126
|
+
reverse=True,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def percentage(value: float) -> str:
|
|
131
|
+
return f"{value * 100:.1f}%"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def markdown(candidates: Iterable[dict[str, float | str]]) -> str:
|
|
135
|
+
lines = [
|
|
136
|
+
"| URL | Clicks (current / previous) | Click change | Impressions change | Position worsening |",
|
|
137
|
+
"| --- | ---: | ---: | ---: | ---: |",
|
|
138
|
+
]
|
|
139
|
+
for candidate in candidates:
|
|
140
|
+
lines.append(
|
|
141
|
+
"| {url} | {current_clicks:.0f} / {previous_clicks:.0f} | {click_change} | {impression_change} | {position_worsening:+.1f} |".format(
|
|
142
|
+
url=candidate["url"],
|
|
143
|
+
current_clicks=float(candidate["current_clicks"]),
|
|
144
|
+
previous_clicks=float(candidate["previous_clicks"]),
|
|
145
|
+
click_change=percentage(float(candidate["click_change"])),
|
|
146
|
+
impression_change=percentage(float(candidate["impression_change"])),
|
|
147
|
+
position_worsening=float(candidate["position_worsening"]),
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
return "\n".join(lines)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def main() -> int:
|
|
154
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
155
|
+
parser.add_argument(
|
|
156
|
+
"input", help="Normalized CSV path, or - to read CSV from standard input."
|
|
157
|
+
)
|
|
158
|
+
parser.add_argument("--min-previous-clicks", type=float, default=100)
|
|
159
|
+
parser.add_argument("--max-click-change", type=float, default=-0.20)
|
|
160
|
+
parser.add_argument("--max-absolute-impression-change", type=float, default=0.25)
|
|
161
|
+
parser.add_argument("--min-position-worsening", type=float, default=0.5)
|
|
162
|
+
parser.add_argument("--min-click-gap-vs-impressions", type=float, default=0.10)
|
|
163
|
+
parser.add_argument("--format", choices=("markdown", "csv"), default="markdown")
|
|
164
|
+
arguments = parser.parse_args()
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
if arguments.input == "-":
|
|
168
|
+
candidates = select_candidates(
|
|
169
|
+
read_rows(sys.stdin),
|
|
170
|
+
arguments.min_previous_clicks,
|
|
171
|
+
arguments.max_click_change,
|
|
172
|
+
arguments.max_absolute_impression_change,
|
|
173
|
+
arguments.min_position_worsening,
|
|
174
|
+
arguments.min_click_gap_vs_impressions,
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
with Path(arguments.input).open(
|
|
178
|
+
encoding="utf-8-sig", newline=""
|
|
179
|
+
) as input_file:
|
|
180
|
+
candidates = select_candidates(
|
|
181
|
+
read_rows(input_file),
|
|
182
|
+
arguments.min_previous_clicks,
|
|
183
|
+
arguments.max_click_change,
|
|
184
|
+
arguments.max_absolute_impression_change,
|
|
185
|
+
arguments.min_position_worsening,
|
|
186
|
+
arguments.min_click_gap_vs_impressions,
|
|
187
|
+
)
|
|
188
|
+
except (OSError, ValueError, csv.Error) as error:
|
|
189
|
+
parser.error(str(error))
|
|
190
|
+
|
|
191
|
+
if arguments.format == "markdown":
|
|
192
|
+
print(markdown(candidates))
|
|
193
|
+
return 0
|
|
194
|
+
|
|
195
|
+
writer = csv.DictWriter(
|
|
196
|
+
sys.stdout,
|
|
197
|
+
fieldnames=(
|
|
198
|
+
"url",
|
|
199
|
+
*REQUIRED_COLUMNS[1:],
|
|
200
|
+
"click_change",
|
|
201
|
+
"impression_change",
|
|
202
|
+
"position_worsening",
|
|
203
|
+
"click_gap_vs_impressions",
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
writer.writeheader()
|
|
207
|
+
writer.writerows(candidates)
|
|
208
|
+
return 0
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
if __name__ == "__main__":
|
|
212
|
+
raise SystemExit(main())
|
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: prove-it-diagnostics
|
|
3
|
-
description: Prove it for debugging, performance investigation, bottleneck analysis, profiling, and optimization work. When reading code does not prove the cause, run a minimal reproduction, add temporary traces, measure bottlenecks, and verify the fix or improvement.
|
|
4
|
-
license: MIT
|
|
5
|
-
metadata:
|
|
6
|
-
author: Robot-Inventor
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Prove It Diagnostics
|
|
10
|
-
|
|
11
|
-
Use this skill to pinpoint the causes of bugs and performance bottlenecks and fix them. Reading code alone may not reveal the source of a bug or bottleneck. Do not claim that a speculative code change fixed a bug or improved performance. Run the code, gather evidence, identify the cause, and verify the fix.
|
|
12
|
-
|
|
13
|
-
## Steps
|
|
14
|
-
|
|
15
|
-
The following is the core of this skill; please follow the specified steps.
|
|
16
|
-
|
|
17
|
-
## 1. Establish a performance baseline for optimization tasks
|
|
18
|
-
|
|
19
|
-
For performance optimization tasks, create and run a benchmark before making changes. Run the benchmark as many times as practical. Use averages, percentiles, and other relevant statistics to establish a stable baseline instead of relying on a single run.
|
|
20
|
-
|
|
21
|
-
## 2. Read the code
|
|
22
|
-
|
|
23
|
-
Read the code related to the bug or performance issue. For debugging tasks, you may fix the issue and skip the remaining steps when you find a clear cause, such as an incorrect operator or an obvious contradiction in the logic.
|
|
24
|
-
|
|
25
|
-
Continue with the remaining steps when you cannot identify the cause with confidence or when you find suspicious code but lack enough evidence to blame it.
|
|
26
|
-
|
|
27
|
-
### 3. Verify the cause
|
|
28
|
-
|
|
29
|
-
Do not rely on code inspection alone. Run experiments to identify the source of the bug or bottleneck.
|
|
30
|
-
|
|
31
|
-
For debugging tasks:
|
|
32
|
-
|
|
33
|
-
1. Create a reproduction in a temporary file or REPL and run it to confirm the bug. If the reproduction does not fail, add relevant behavior from the target code until it reproduces the bug.
|
|
34
|
-
2. Remove or revert each part of the reproduction to find the smallest program that still triggers the bug. A minimal reproduction narrows the list of possible causes.
|
|
35
|
-
3. Investigate the minimal reproduction, identify the cause, and fix it. Address the root cause instead of applying a superficial workaround, as described in the Rules section.
|
|
36
|
-
4. Confirm that the fix prevents the bug in the minimal reproduction.
|
|
37
|
-
5. Apply the fix to the production code and confirm that it resolves the original bug. If the bug remains, return to the reproduction step and repeat the process.
|
|
38
|
-
|
|
39
|
-
For performance optimization tasks:
|
|
40
|
-
|
|
41
|
-
1. Use benchmarks, profilers, timing logs, or similar tools to identify the slow operation.
|
|
42
|
-
2. Fix the bottleneck, then use the same measurements to confirm that performance improved.
|
|
43
|
-
3. Apply the fix to the production code and confirm the improvement there. If performance does not improve, return to bottleneck analysis and repeat the process.
|
|
44
|
-
|
|
45
|
-
### 4. Clean up
|
|
46
|
-
|
|
47
|
-
Remove temporary logs, reproduction code, generated files, and log data created during verification.
|
|
48
|
-
|
|
49
|
-
## Rules
|
|
50
|
-
|
|
51
|
-
If a user reports that your fix did not resolve the bug or improve performance, treat your previous diagnosis as unverified and restart the investigation.
|
|
52
|
-
|
|
53
|
-
Surface-level and stop-gap fixes are shameful; identify and fix the root cause. For example, when an error triggers a bug, find and fix the source of the error instead of adding a fallback for the failure. Keep asking yourself, “Is this the root cause?”
|
|
54
|
-
|
|
55
|
-
When evidence points to a bug in a dependency, do not patch the dependency. Tell the user that the dependency appears to contain a bug, describe the evidence, and propose changes they can make in their project without modifying the dependency.
|
|
56
|
-
|
|
57
|
-
When you get stuck, try these approaches:
|
|
58
|
-
|
|
59
|
-
- Add temporary logs across the affected path. Use them to find the boundary between correct and incorrect behavior, or between fast and slow execution.
|
|
60
|
-
- When behavior contradicts the code’s logic or you cannot make progress, search the relevant library’s GitHub issues and pull requests, Stack Overflow, and community articles for reports of the same problem and possible solutions.
|
|
1
|
+
---
|
|
2
|
+
name: prove-it-diagnostics
|
|
3
|
+
description: Prove it for debugging, performance investigation, bottleneck analysis, profiling, and optimization work. When reading code does not prove the cause, run a minimal reproduction, add temporary traces, measure bottlenecks, and verify the fix or improvement.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: Robot-Inventor
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Prove It Diagnostics
|
|
10
|
+
|
|
11
|
+
Use this skill to pinpoint the causes of bugs and performance bottlenecks and fix them. Reading code alone may not reveal the source of a bug or bottleneck. Do not claim that a speculative code change fixed a bug or improved performance. Run the code, gather evidence, identify the cause, and verify the fix.
|
|
12
|
+
|
|
13
|
+
## Steps
|
|
14
|
+
|
|
15
|
+
The following is the core of this skill; please follow the specified steps.
|
|
16
|
+
|
|
17
|
+
## 1. Establish a performance baseline for optimization tasks
|
|
18
|
+
|
|
19
|
+
For performance optimization tasks, create and run a benchmark before making changes. Run the benchmark as many times as practical. Use averages, percentiles, and other relevant statistics to establish a stable baseline instead of relying on a single run.
|
|
20
|
+
|
|
21
|
+
## 2. Read the code
|
|
22
|
+
|
|
23
|
+
Read the code related to the bug or performance issue. For debugging tasks, you may fix the issue and skip the remaining steps when you find a clear cause, such as an incorrect operator or an obvious contradiction in the logic.
|
|
24
|
+
|
|
25
|
+
Continue with the remaining steps when you cannot identify the cause with confidence or when you find suspicious code but lack enough evidence to blame it.
|
|
26
|
+
|
|
27
|
+
### 3. Verify the cause
|
|
28
|
+
|
|
29
|
+
Do not rely on code inspection alone. Run experiments to identify the source of the bug or bottleneck.
|
|
30
|
+
|
|
31
|
+
For debugging tasks:
|
|
32
|
+
|
|
33
|
+
1. Create a reproduction in a temporary file or REPL and run it to confirm the bug. If the reproduction does not fail, add relevant behavior from the target code until it reproduces the bug.
|
|
34
|
+
2. Remove or revert each part of the reproduction to find the smallest program that still triggers the bug. A minimal reproduction narrows the list of possible causes.
|
|
35
|
+
3. Investigate the minimal reproduction, identify the cause, and fix it. Address the root cause instead of applying a superficial workaround, as described in the Rules section.
|
|
36
|
+
4. Confirm that the fix prevents the bug in the minimal reproduction.
|
|
37
|
+
5. Apply the fix to the production code and confirm that it resolves the original bug. If the bug remains, return to the reproduction step and repeat the process.
|
|
38
|
+
|
|
39
|
+
For performance optimization tasks:
|
|
40
|
+
|
|
41
|
+
1. Use benchmarks, profilers, timing logs, or similar tools to identify the slow operation.
|
|
42
|
+
2. Fix the bottleneck, then use the same measurements to confirm that performance improved.
|
|
43
|
+
3. Apply the fix to the production code and confirm the improvement there. If performance does not improve, return to bottleneck analysis and repeat the process.
|
|
44
|
+
|
|
45
|
+
### 4. Clean up
|
|
46
|
+
|
|
47
|
+
Remove temporary logs, reproduction code, generated files, and log data created during verification.
|
|
48
|
+
|
|
49
|
+
## Rules
|
|
50
|
+
|
|
51
|
+
If a user reports that your fix did not resolve the bug or improve performance, treat your previous diagnosis as unverified and restart the investigation.
|
|
52
|
+
|
|
53
|
+
Surface-level and stop-gap fixes are shameful; identify and fix the root cause. For example, when an error triggers a bug, find and fix the source of the error instead of adding a fallback for the failure. Keep asking yourself, “Is this the root cause?”
|
|
54
|
+
|
|
55
|
+
When evidence points to a bug in a dependency, do not patch the dependency. Tell the user that the dependency appears to contain a bug, describe the evidence, and propose changes they can make in their project without modifying the dependency.
|
|
56
|
+
|
|
57
|
+
When you get stuck, try these approaches:
|
|
58
|
+
|
|
59
|
+
- Add temporary logs across the affected path. Use them to find the boundary between correct and incorrect behavior, or between fast and slow execution.
|
|
60
|
+
- When behavior contradicts the code’s logic or you cannot make progress, search the relevant library’s GitHub issues and pull requests, Stack Overflow, and community articles for reports of the same problem and possible solutions.
|