indexgap 1.5.0__tar.gz

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.
Files changed (61) hide show
  1. indexgap-1.5.0/LICENSE +21 -0
  2. indexgap-1.5.0/PKG-INFO +399 -0
  3. indexgap-1.5.0/README.md +372 -0
  4. indexgap-1.5.0/indexgap/__init__.py +2 -0
  5. indexgap-1.5.0/indexgap/__main__.py +9 -0
  6. indexgap-1.5.0/indexgap/aeo.py +365 -0
  7. indexgap-1.5.0/indexgap/checks.py +678 -0
  8. indexgap-1.5.0/indexgap/cite.py +335 -0
  9. indexgap-1.5.0/indexgap/cli.py +1083 -0
  10. indexgap-1.5.0/indexgap/content.py +517 -0
  11. indexgap-1.5.0/indexgap/core.py +835 -0
  12. indexgap-1.5.0/indexgap/doctor.py +461 -0
  13. indexgap-1.5.0/indexgap/engines.py +179 -0
  14. indexgap-1.5.0/indexgap/freshness.py +130 -0
  15. indexgap-1.5.0/indexgap/generate.py +453 -0
  16. indexgap-1.5.0/indexgap/hreflang.py +324 -0
  17. indexgap-1.5.0/indexgap/i18n.py +133 -0
  18. indexgap-1.5.0/indexgap/install.py +409 -0
  19. indexgap-1.5.0/indexgap/locale/__init__.py +8 -0
  20. indexgap-1.5.0/indexgap/locale/en/__init__.py +14 -0
  21. indexgap-1.5.0/indexgap/locale/en/checks.py +163 -0
  22. indexgap-1.5.0/indexgap/locale/en/cite.py +100 -0
  23. indexgap-1.5.0/indexgap/locale/en/cli.py +281 -0
  24. indexgap-1.5.0/indexgap/locale/en/content.py +89 -0
  25. indexgap-1.5.0/indexgap/locale/en/core.py +135 -0
  26. indexgap-1.5.0/indexgap/locale/en/doctor.py +168 -0
  27. indexgap-1.5.0/indexgap/locale/en/hreflang.py +116 -0
  28. indexgap-1.5.0/indexgap/locale/en/repair.py +229 -0
  29. indexgap-1.5.0/indexgap/locale/en/report.py +223 -0
  30. indexgap-1.5.0/indexgap/portfolio.py +268 -0
  31. indexgap-1.5.0/indexgap/profiles.py +198 -0
  32. indexgap-1.5.0/indexgap/publish.py +313 -0
  33. indexgap-1.5.0/indexgap/repair.py +343 -0
  34. indexgap-1.5.0/indexgap/report.py +431 -0
  35. indexgap-1.5.0/indexgap/settings.py +222 -0
  36. indexgap-1.5.0/indexgap/skills/indexgap-plan/SKILL.en.md +97 -0
  37. indexgap-1.5.0/indexgap/skills/indexgap-plan/SKILL.md +97 -0
  38. indexgap-1.5.0/indexgap/skills/indexgap-portfolio/SKILL.en.md +94 -0
  39. indexgap-1.5.0/indexgap/skills/indexgap-portfolio/SKILL.md +90 -0
  40. indexgap-1.5.0/indexgap/skills/indexgap-publish/SKILL.en.md +151 -0
  41. indexgap-1.5.0/indexgap/skills/indexgap-publish/SKILL.md +147 -0
  42. indexgap-1.5.0/indexgap/skills/indexgap-review/SKILL.en.md +139 -0
  43. indexgap-1.5.0/indexgap/skills/indexgap-review/SKILL.md +137 -0
  44. indexgap-1.5.0/indexgap/sources.py +421 -0
  45. indexgap-1.5.0/indexgap.egg-info/PKG-INFO +399 -0
  46. indexgap-1.5.0/indexgap.egg-info/SOURCES.txt +59 -0
  47. indexgap-1.5.0/indexgap.egg-info/dependency_links.txt +1 -0
  48. indexgap-1.5.0/indexgap.egg-info/entry_points.txt +2 -0
  49. indexgap-1.5.0/indexgap.egg-info/top_level.txt +1 -0
  50. indexgap-1.5.0/pyproject.toml +44 -0
  51. indexgap-1.5.0/setup.cfg +4 -0
  52. indexgap-1.5.0/tests/test_cite.py +214 -0
  53. indexgap-1.5.0/tests/test_i18n.py +302 -0
  54. indexgap-1.5.0/tests/test_install.py +299 -0
  55. indexgap-1.5.0/tests/test_live.py +226 -0
  56. indexgap-1.5.0/tests/test_multilingual.py +288 -0
  57. indexgap-1.5.0/tests/test_portfolio.py +246 -0
  58. indexgap-1.5.0/tests/test_regressions.py +733 -0
  59. indexgap-1.5.0/tests/test_repair.py +336 -0
  60. indexgap-1.5.0/tests/test_sources.py +250 -0
  61. indexgap-1.5.0/tests/test_wave2.py +466 -0
indexgap-1.5.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,399 @@
1
+ Metadata-Version: 2.4
2
+ Name: indexgap
3
+ Version: 1.5.0
4
+ Summary: Lint your programmatic SEO pipeline — from keywords to indexed pages. No dependencies.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/borisowlexa2010-star/IndexGap
7
+ Project-URL: Changelog, https://github.com/borisowlexa2010-star/IndexGap/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/borisowlexa2010-star/IndexGap/issues
9
+ Keywords: seo,programmatic-seo,sitemap,indexnow,aeo,geo,search-console,site-audit,lint
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
22
+ Classifier: Topic :: Text Processing :: Markup :: HTML
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Dynamic: license-file
27
+
28
+ # indexgap
29
+
30
+ **Lint your programmatic SEO pipeline — from keywords to indexed pages.**
31
+
32
+ > [Русская версия](README.ru.md)
33
+
34
+ You generated three thousand pages from a dataset. They're on disk, they're in
35
+ the sitemap, and there's no traffic. `indexgap` tells you where they were lost.
36
+
37
+ Pure Python 3.9+ standard library. No dependencies, no API keys, no paid
38
+ subscriptions. Nothing leaves your machine without an explicit flag.
39
+
40
+ ---
41
+
42
+ ## Why this exists
43
+
44
+ Plenty of tools audit *a page*. A generated pipeline breaks differently —
45
+ systematically and quietly:
46
+
47
+ * a page is in the sitemap, but no internal link points to it, so the crawler
48
+ never arrives;
49
+ * three hundred pages differ by five words, and the search engine keeps one;
50
+ * the model wrote "12 years in business" and "3,500 orders" — neither number
51
+ exists in any row of your data, but it reads convincingly;
52
+ * `lastmod` equals the build date on every page, so it means nothing;
53
+ * two keywords with the same intent produced two competing pages.
54
+
55
+ None of this is visible page by page. Each one looks fine on its own.
56
+
57
+ ---
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ pip install indexgap # once the first PyPI release is out
63
+
64
+ # until then — from source:
65
+ git clone https://github.com/borisowlexa2010-star/IndexGap && cd IndexGap
66
+ pip install -e .
67
+ ```
68
+
69
+ Then, once per project:
70
+
71
+ ```bash
72
+ cd ~/projects/my-site
73
+ indexgap init
74
+ ```
75
+
76
+ `init` reads the project and records what makes it different: where the pages
77
+ are, the site URL, the content type, the dataset. It installs skills into
78
+ `.claude/skills/` so your coding agent picks them up on its own, and adds the
79
+ working files to `.gitignore`.
80
+
81
+ Nothing project-specific is ever copied between projects — it is detected
82
+ fresh each time. **The IndexNow key in particular is never carried over:**
83
+ it is bound to one domain by a file at that site's root, and a borrowed key
84
+ returns 403. `indexgap init --key` mints a new one for this project.
85
+
86
+ After that, the daily command is just:
87
+
88
+ ```bash
89
+ indexgap check
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Commands
95
+
96
+ ```bash
97
+ indexgap init # install into this project
98
+ indexgap plan keywords.csv # audit the keyword set before generating
99
+ indexgap check # everything local: text, structure, machine-readability
100
+ indexgap brief --write # turn the findings into work orders beside the pages
101
+ indexgap sitemap --out-dir ./public # sitemap with sharding and an honest lastmod
102
+ indexgap notify --key <your-key> # tell IndexNow what actually changed
103
+ indexgap doctor --sitemap ./public/sitemap.xml --indexed gsc.csv
104
+ indexgap portfolio projects.json # every site you own, in one run
105
+ indexgap profiles # what the content-type presets change
106
+ indexgap cite --domain example.com # do AI answers cite you? (your own API keys)
107
+ ```
108
+
109
+ Output is English by default, Russian with `--lang ru` (or `INDEXGAP_LANG`, or
110
+ your system locale). That includes the HTML report, every finding description,
111
+ `--help`, and the skills `indexgap init` installs into your project.
112
+
113
+ Understands both built HTML and Markdown sources with frontmatter. Input file
114
+ encoding is detected, not assumed: UTF-16, BOM, and the cp1251 CSVs that
115
+ Russian Excel produces all read correctly. Exports are read as they come —
116
+ CSV with any delimiter, XLSX straight from Ahrefs or Semrush without
117
+ re-saving, JSON, NDJSON, an XML sitemap, or a plain list of URLs — and the
118
+ keyword column is found whether the export calls it `Keyword`, `Фраза`,
119
+ `Запрос` or `Search Term`.
120
+
121
+ ---
122
+
123
+ ## What it checks
124
+
125
+ **Before generating** — exact duplicate keys, slug collisions and *same-intent*
126
+ keys. That last one matters most: two keywords meaning the same thing will
127
+ produce two competing pages, and it is cheaper never to create the second.
128
+
129
+ **After generating — the text**
130
+
131
+ | Check | Why it matters |
132
+ |---|---|
133
+ | **Numbers absent from your data** | prices, terms and counts are verified against the source row. A number that appears nowhere in the dataset is never forgiven, no matter how many pages repeat it |
134
+ | Identical heading skeletons | different words, same structure — a stamping tell |
135
+ | Identical opening sentences | the second tell |
136
+ | Leftover brief, `status: draft` | unfinished pages never reach the sitemap or the IndexNow queue |
137
+ | "Click here" anchors | a link with no meaning in it |
138
+
139
+ **After generating — the structure**
140
+
141
+ Near-duplicates (exact pairwise below 400 pages, MinHash + LSH above), share of
142
+ unique text measured *by bigrams* rather than words, thin pages, orphans,
143
+ click depth, `noindex`, `nosnippet`, foreign canonicals, missing or duplicate
144
+ H1, duplicate titles and descriptions.
145
+
146
+ **Machine readability for AI search**
147
+
148
+ Snippet controls (`nosnippet`, `max-snippet:0`), `robots.txt` rules for
149
+ OAI-SearchBot, PerplexityBot, ClaudeBot, GPTBot and Google-Extended (they are
150
+ not interchangeable — blocking OAI-SearchBot removes you from ChatGPT search
151
+ answers, while Google-Extended does not affect AI Overviews), empty JS shells,
152
+ a direct answer in the first paragraph, question-shaped subheadings, valid
153
+ JSON-LD that matches the visible text, machine-readable dates and author.
154
+
155
+ **At publish time** — sitemap sharding past 45,000 URLs and a `lastmod` that
156
+ changes only when the text, title or description changed. Editing one menu item
157
+ does not mark the whole site as modified. IndexNow sends only what changed.
158
+
159
+ ---
160
+
161
+ ## From findings to work orders
162
+
163
+ A report answers "what is wrong with me". `indexgap brief` answers "what do I
164
+ do" — it lays the same findings out as markdown files next to the pages they
165
+ belong to, each with an imperative fix, the thresholds the fix has to meet, and
166
+ the dataset row as the only permitted source of numbers.
167
+
168
+ ```bash
169
+ indexgap brief --dataset keywords.csv # dry run: says what it would write
170
+ indexgap brief --dataset keywords.csv --write
171
+ ```
172
+
173
+ On the live 2,970-page catalogue the check reports 6,074 findings and `brief`
174
+ writes 966 work orders. The difference is three placement rules, and they are
175
+ the whole point:
176
+
177
+ * **A property of the template gets one brief, not 2,919.** `no-question-headings`
178
+ fired on 2,919 of 2,970 pages. That is one edit to one template, and 2,919
179
+ identical tasks would bury the 51 pages that actually differ. The same holds
180
+ per language: a finding on every page of the Chinese version is 10% of the
181
+ site but still one template fix.
182
+ * **Near-duplicates are fixed as a group.** 588 duplicate findings are 72
183
+ groups. One page out of a group of 17 cannot be fixed alone — the brief goes
184
+ to the group: keep one, pull the rest apart by intent, and never link them to
185
+ each other.
186
+ * **robots.txt, markup and hreflang clusters belong to the site**, not to a
187
+ page, and live in their own file.
188
+
189
+ `--limit` (50 by default) writes the heaviest pages first; 892 briefs is not a
190
+ task list, it is a second report. Nothing is created without `--write`.
191
+
192
+ **The package writes no text, on purpose.** Its central check compares the
193
+ numbers on a page against the dataset row that produced it. If the package
194
+ supplied those numbers itself, the check would be checking its own output and
195
+ would always be green. So `brief` states the task and stops; a person or an
196
+ agent writes the words.
197
+
198
+ ---
199
+
200
+ ## The funnel
201
+
202
+ The reason to install it at all:
203
+
204
+ ```
205
+ Generated 58
206
+ Indexable 56 (−2: noindex or a canonical pointing elsewhere)
207
+ In sitemap 56
208
+ In at least one index 28 (−28: the engine knows the URL and didn't add it)
209
+
210
+ Why pages are not indexed:
211
+ 7 orphaned or unreachable → link them from hub pages
212
+ 12 deeper than the click budget → move them up
213
+ 9 no local explanation → check status in Search Console
214
+ ```
215
+
216
+ Index data comes from ordinary exports — no API keys. A webmaster panel is the
217
+ direct source (Search Console, Bing Webmaster Tools, Yandex.Webmaster, Naver,
218
+ Seznam), but not everyone has one, so exports from **Ahrefs, Semrush, Serpstat,
219
+ Moz, Screaming Frog, Sitebulb, JetOctopus, OnCrawl, Netpeak, GA4, Matomo,
220
+ Plausible** — or a plain list of URLs — are read too, in CSV, XLSX, JSON,
221
+ NDJSON or XML.
222
+
223
+ They are not interchangeable, and the tool refuses to pretend otherwise. A
224
+ panel answers "does the engine know this page". Analytics proves a page is
225
+ indexed, but only for pages someone actually visited — its silence proves
226
+ nothing. A crawler proves reachability, not indexation. Ahrefs and Semrush are
227
+ *their* index, not Google's. So the funnel step is renamed to match the
228
+ evidence — "at least in one index" versus "known to a third-party service" —
229
+ and when a crawler export sits next to a panel, the report says out loud that
230
+ the step count is higher than real indexation. Engine-vs-engine comparison
231
+ runs over panels only.
232
+
233
+ **Ask for all the panels you have, not just Google.** A page missing *everywhere* is a technical problem. A page missing
234
+ *only in one engine* was crawled and accepted by the others, which makes it a
235
+ quality or speed question that technical fixes rarely solve. Without the split
236
+ the two look identical and people fix the wrong thing.
237
+
238
+ One honest caveat the tool states out loud: the Search Console "Pages" export
239
+ is an *impressions* report, not an index report. A page that is indexed but has
240
+ no impressions won't appear in it, so on a young site the funnel overstates
241
+ losses.
242
+
243
+ ---
244
+
245
+ ## Multilingual and multi-region
246
+
247
+ A programmatic pipeline is usually multilingual, and that breaks in ways a
248
+ single-language site never does.
249
+
250
+ Text volume and title/description length are measured by each page's **script**,
251
+ in display width — not by one language detected for the whole project. That
252
+ distinction is not academic: on a live 2,970-page catalogue in ten languages the
253
+ project language came out as `en`, every Chinese page was counted as English,
254
+ and all 174 `thin` findings were false. Width also handles mixed strings, which
255
+ is what real pages are: those Chinese titles are 43% Han and the rest Latin.
256
+
257
+ Anchor length is judged only in scripts where length means something. All 914
258
+ `vague-anchor` findings on that same site were false — "यमन" (Yemen) and "হোম"
259
+ (Home) are three characters and complete words.
260
+
261
+ **hreflang** is checked as a cluster: a missing self-reference, a one-way link
262
+ (Google discards the whole cluster rather than counting it partly), an alternate
263
+ that is noindex or canonicalised elsewhere, a canonical that leaves the language
264
+ and cancels the cluster, a missing `x-default`, and language codes where a
265
+ country code was meant — `uk` is Ukrainian, not the United Kingdom. On a
266
+ monolingual site none of this runs.
267
+
268
+ And `en-us` versus `en-gb` is **not** a duplicate. Those pages are legitimately
269
+ near-identical; "keep one and set a canonical" would delete the regional
270
+ version. Pairs inside one hreflang cluster are excluded from the duplicate
271
+ check and reported separately.
272
+
273
+ ---
274
+
275
+ ## Content-type profiles
276
+
277
+ Thresholds differ across content types by substance, not taste. 250 words is
278
+ normal for a guide and absurd for an event card; fact-checking is meaningless
279
+ where there is no dataset at all.
280
+
281
+ | Profile | For | What changes |
282
+ |---|---|---|
283
+ | `catalog` | pages generated from data rows | fact-checking is primary; duplicate threshold 0.80; thin under 250 words |
284
+ | `events` | listings, schedules, venues | threshold 0.88 — two dates of one tour are legitimately similar; adds `stale-event` |
285
+ | `ugc` | feeds, threads, reviews | fact-checking is switched off *and says so*; threshold 0.92 |
286
+ | `product` | dozens of landing pages, not thousands | duplicates aren't the issue; the AI-readability checks are |
287
+
288
+ `stale-event` catches an event whose date has passed while the page stays open
289
+ to indexing. That isn't a traffic problem, it's a trust problem: someone drives
290
+ to a concert that no longer exists.
291
+
292
+ For `ugc`, silence is not a clean bill of health — it means there was nothing to
293
+ check against, and the tool prints that as a line rather than leaving you to
294
+ assume.
295
+
296
+ ---
297
+
298
+ ## Portfolio
299
+
300
+ Separate reports answer "what's wrong with this site". A portfolio answers the
301
+ question you can't see in them: **what breaks the same way everywhere.**
302
+
303
+ ```
304
+ · visa 2933 pages critical 412 [catalog]
305
+ · events 840 pages critical 61 [events]
306
+ · feed 5100 pages critical 0 [ugc]
307
+ · product 34 pages critical 2 [product]
308
+
309
+ Shared problems, as a share of each project's pages:
310
+ orphan 3 projects: visa 41%, events 38%, product 35%
311
+ same-opening 2 projects: visa 62%, events 55%
312
+ ```
313
+
314
+ Shares, not counts: a hundred findings across three thousand pages and ten
315
+ across twenty are the same disease at different volumes. One project failing
316
+ doesn't stop the run — it becomes a line in the report.
317
+
318
+ ---
319
+
320
+ ## What it does *not* claim
321
+
322
+ It does not promise citations in AI search, and it says so in the output.
323
+ Ahrefs, across 75,000 brands, found AI visibility correlates most with mentions
324
+ *off* your site (0.66–0.74) and with page count at **0.19** — which is exactly
325
+ what a programmatic pipeline produces. 76% of AI Overview citations come from
326
+ pages already ranking in the classic top 10.
327
+
328
+ So: machine readability is a necessary condition and this tool's job. Getting
329
+ cited is decided by work outside your files, and that is not a code problem.
330
+
331
+ `indexgap cite` measures the thermometer, and says so: it asks Perplexity, the
332
+ OpenAI Responses API, the Gemini API and Grok a set of real questions and counts
333
+ how often your domain comes back in the sources. It reports a share of runs, not
334
+ yes/no, because these answers are not deterministic. And it never says "ChatGPT
335
+ cites you" — what it measured is the API, which OpenAI's own docs describe as
336
+ behaving differently from the product. It is the only command that needs API
337
+ keys, it is off by default, and it sends nothing without `--send`.
338
+
339
+ No `llms.txt` generator either. Google has stated it does not support it and
340
+ has no plans to; no engine has confirmed using it for ranking. Generating a
341
+ file nobody reads is a ritual, not a feature.
342
+
343
+ It also doesn't write or rewrite content, check rankings, or call paid services.
344
+ `indexgap brief` formulates the task and stops there — on purpose. The central
345
+ check compares the numbers on a page against the dataset row that produced it;
346
+ if the package supplied those numbers itself, the check would be checking its
347
+ own output and would always be green.
348
+ Rejecting keywords and acting on contested findings is always confirmed by a human.
349
+
350
+ And it does not replace Search Console — it leads you there. If the tool says
351
+ one thing and Search Console says another, Search Console is right.
352
+
353
+ ---
354
+
355
+ ## Tests
356
+
357
+ ```bash
358
+ python3 -m unittest discover -s tests
359
+ ```
360
+
361
+ 301 scenarios. Each one is a reproduced defect found by two waves of adversarial
362
+ review and one run against six live sites, plus the behaviour of profiles,
363
+ portfolio and project installation. The rule: a finding without a test comes back.
364
+
365
+ ## Calibrated on live sites, not fixtures
366
+
367
+ Thresholds are not guesses. They were checked against six production sites —
368
+ 7,149 sitemap URLs, 5,041 pages fetched and parsed — and the run changed the
369
+ tool in three ways:
370
+
371
+ * **One cause, not four findings.** Two of the six sites served every page as an
372
+ empty JavaScript shell. The tool reported 1,099 `js-shell` *and* 1,099
373
+ `low-uniqueness` *and* 1,098 `orphan` — one disease counted four times. Checks
374
+ that need text or links are now skipped on a shell, and the run says how many
375
+ shells there were.
376
+ * **Duplicates are groups, not pages.** 588 near-duplicate pages turned out to be
377
+ 72 groups, the largest holding 24. "Rewrite 588 pages" is a sentence;
378
+ "untangle 72 topics" is a task.
379
+ * **A finding on every page is a template property.** `vague-anchor` fired on
380
+ 2,970 of 2,970 pages — the culprits were a language switcher (`中文`) and a
381
+ social link (`VK`), short in characters and perfectly informative. Anchor
382
+ length is now measured in the right unit, and any code that hits ≥90% of pages
383
+ is labelled as something to fix once in the template.
384
+
385
+ ---
386
+
387
+ ## For agents
388
+
389
+ `SKILL.md` at the root is the overview. `indexgap/skills/` holds four
390
+ stage-specific skills — `indexgap-plan`, `indexgap-review`, `indexgap-publish`,
391
+ `indexgap-portfolio` — which `indexgap init` copies into your project's
392
+ `.claude/skills/`. A narrow skill fires more accurately than a broad one:
393
+ the agent sees only the commands and finding codes for the stage it's in.
394
+
395
+ Codex users: `indexgap init --agents` writes a marked block into `AGENTS.md`.
396
+
397
+ ---
398
+
399
+ MIT.