@goflag/cli 0.2.5 → 0.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 CHANGED
@@ -16,9 +16,14 @@ on the site. A human cannot check those on 400 pages. That is the whole job.
16
16
  ## What it checks
17
17
 
18
18
  - **Broken links** — scrapes every crawled page, dedupes targets globally (a footer link on 500 pages is probed once), and checks each with `HEAD`→`GET` fallback, redirect-chain/loop detection, soft-404 and anti-bot (403/429) triage. Broken links are mapped back to the pages that reference them.
19
- - **Missing translation pages** — builds a `route × locale` matrix from the crawl and flags every route that exists in one locale but is missing in another, plus hreflang reciprocity gaps (`A → B` with no `B → A`), missing `x-default`, and invalid locale tags.
19
+ - **Missing translation pages** — builds a `route × locale` matrix and flags every route that exists in one locale but is missing in another, plus hreflang reciprocity gaps (`A → B` with no `B → A`), missing `x-default`, and locale tags naming a language, script or region that does not exist. Rows are keyed by pathname, unless the site declares its translation clusters — either with `xhtml:link` in the sitemap, or with reciprocal `hreflang` between two crawled pages' `<head>`. Then those declarations decide which URLs are one page, whatever their slugs, and the run reports `diagnostics.declaredClusters`. The sitemap source survives `--coverage structural`, where the two locales of a slug-translating family are rarely both sampled; the `<head>` source needs both pages in hand and is what fixes the common site that declares properly there and nothing in its sitemap.
20
20
  - **robots.txt policy** — flags a site that forbids crawling while its pages ask to be indexed. The two declarations cannot both hold, and robots.txt wins, so the meta tag is never even read.
21
21
  - **SEO metadata** — lints each page's `<head>` for the handful of mistakes that actually hurt in search and social and are invisible in a browser: missing/oversized `<title>` and description, missing/relative canonical, missing `og:title`/`og:description`/`og:image`, missing viewport, and contradictory `robots`/`googlebot`/`X-Robots-Tag` directives.
22
+ - **Unreachable pages** — a page the crawl reached that answered non-2xx, or that never answered at all (timeout, reset, DNS failure — recorded as `status: 0`), is reported in `unreachablePages`. goflag asks a second time, from the back of the queue, before calling a page unreachable. Each one is an `error`-severity finding: it turns the verdict red, counts toward `--max-debt`, and fails the build even under `--fail-on error`. A page that silently dropped out of the audited set is what poisons a baseline, so it is reported rather than warned about.
23
+
24
+ **Which pages get audited.** When goflag finds a sitemap it does not audit every URL in it. It groups URLs that share a path shape — and therefore a template — into families, audits every page that stands alone, and samples three pages per family. A cap answers "how many" and never "which": on a site of thousands of pages built from thirty templates, the first 200 pages a crawl reaches are four templates out of thirty.
25
+
26
+ The trade is stated in every report rather than hidden. Template rules (`canonical.*`, `hreflang.*`, `og.image.missing`) are conclusive on a sampled family; copy rules (`title.length`, `description.length`), broken links and the translation matrix are only conclusive on the pages that were drawn. The terminal prints a `COVERAGE` line, and `diagnostics.coverage` carries `mode`, `considered`, `selected` and every sampled family with its ratio. Pass `--coverage all` to audit what the sitemap lists in order, up to `--max-pages`, which is the pre-0.2.3 behaviour.
22
27
 
23
28
  Pages that declare a `<link rel="canonical">` pointing at another crawled page are excluded from the checks — the site has said they are duplicates, so linting them would multiply every finding by the number of filtered variants. The count is reported as `diagnostics.duplicatePages`.
24
29
 
@@ -125,16 +130,69 @@ npx @goflag/cli https://example.com --report report.json # write JSON to a
125
130
 
126
131
  The terminal view is just a render of that JSON.
127
132
 
133
+ ### The catalogue is data too
134
+
135
+ ```sh
136
+ npx @goflag/cli rules > rules.json
137
+ ```
138
+
139
+ `rules` answers a question about goflag rather than about a site: no URL, no
140
+ crawl, no network. Every entry carries its scope, severity and summary; the
141
+ eleven page rules and four prose rules also carry a rigor, the documents they
142
+ cite and — where a remedy is a line of code — a fix snippet. The three site
143
+ rules carry none of those three: `SiteRule` has no rigor field yet, and the
144
+ catalogue emits `rigor: null` with an empty `sources` rather than inventing an
145
+ authority nobody assigned. Write your consumer against that. The same document
146
+ ships inside the package as `rules.json`, if you would rather read it than run
147
+ anything.
148
+
149
+ The flag list below is data too:
150
+
151
+ ```sh
152
+ npx @goflag/cli flags > flags.json
153
+ ```
154
+
155
+ Same idea, one step further: `flags` prints the very table `goflag --help` is
156
+ rendered from and the argument parser dispatches on, so a flag cannot exist in
157
+ one and not the other. Each entry carries its long and short forms, its argument
158
+ placeholder, its default, the group it belongs to and any flag it requires. It
159
+ ships inside the package as `flags.json` as well.
160
+
161
+ It deliberately omits the message a finding prints — that is built at audit time
162
+ from what the page actually says, so a static copy would be a sample rather than
163
+ a fact, and a sample presented as the truth is how this project's own
164
+ documentation site came to quote a message the engine had stopped printing.
165
+
128
166
  ### Options
129
167
 
130
168
  ```
131
169
  --json Print the JSON report to stdout (nothing else).
170
+ --summary, -s Roll findings up (dedup by link/rule/code). Pairs with
171
+ --json for a compact payload; --report always writes
172
+ the full report regardless.
132
173
  --report <file> Write the JSON report to <file>.
133
- --depth <n> Crawl depth (0 = entry page only). Default: 2.
134
- --max-pages <n> Hard cap on pages crawled. Default: 200.
174
+ --depth <n> How far to follow links out of each page (0 = follow
175
+ none). Sitemap URLs are seeded regardless, so --depth 0
176
+ alone is not "entry page only" — add --no-sitemap.
177
+ Default: 2.
178
+ --max-pages <n> Page budget for the crawl. Default: 200. A hard cap
179
+ under --coverage all; under structural coverage the
180
+ selection wins and the budget is max(<n>, selected + 5),
181
+ so a lower value cannot cut the run short.
182
+ --coverage <mode> How pages are chosen: "structural" (default when a
183
+ sitemap is found) keeps every standalone page and
184
+ samples three pages per family of template-generated
185
+ pages; "all" audits what the sitemap lists, in order,
186
+ up to --max-pages.
135
187
  --include <glob> Only crawl paths matching <glob> (repeatable).
136
188
  --exclude <glob> Skip paths matching <glob> (repeatable).
137
189
  --locales <list> Comma-separated locales the site serves ("fr,en,pt-br").
190
+ Unioned with the prefixes your sitemap shows, never
191
+ substituted for them, so a locale the sitemap serves
192
+ stays on the axis whether or not you list it. Also what
193
+ folds /en/… and /fr/… into one route family for
194
+ structural coverage — pass it on any locale-prefixed
195
+ site.
138
196
  --ignore-holes <glob> Route deliberately not translated everywhere
139
197
  (repeatable).
140
198
  --no-sitemap Do not discover the sitemap; crawl from <url> only.
@@ -160,7 +218,11 @@ The terminal view is just a render of that JSON.
160
218
  --no-external Do not probe off-origin (external) links.
161
219
  --static Static HTML only; never launch headless Chromium.
162
220
  --allow-insecure-tls Accept self-signed / invalid TLS (localhost, tunnels).
163
- --timeout <ms> Per-request timeout in ms. Default: 8000.
221
+ --timeout <ms> Per-request timeout in ms, for page fetches and link
222
+ probes alike. Unset, the defaults differ: 8000 for link
223
+ probes, 15000 for page fetches.
224
+ --verbose, -V Log every page as it is analyzed.
225
+ --quiet, -q Suppress the live progress output.
164
226
  --no-color Disable coloured output.
165
227
  -h, --help Show help.
166
228
  -v, --version Show the version.
@@ -351,9 +413,16 @@ goflag https://example.com --locales fr,en,pt-br
351
413
 
352
414
  When a site publishes no usable sitemap and you pass no `--locales`, goflag
353
415
  does **not** guess the locale axis — it reports the prefixes it saw with the
354
- evidence for each and turns the i18n checks off. Guessing from path shape alone
355
- once turned `/cv` (a CV page served in French) into a locale, because `cv` is a
356
- registered ISO 639-1 code, and produced 31 findings that were never real.
416
+ evidence for each and turns the axis-dependent checks off (holes,
417
+ `hreflang.missing`, `hreflang.sitemap-mismatch`; the reciprocity checks read the
418
+ markup and keep running). Guessing from path shape alone once turned `/cv` (a CV
419
+ page served in French) into a locale, because `cv` is a registered ISO 639-1
420
+ code, and produced 31 findings that were never real.
421
+
422
+ Pass `--locales` even when the sitemap is fine. It is the axis goflag folds when
423
+ it groups URLs into route families, so without it a site with many top-level
424
+ sections under `/en` can have those sections sampled as one family instead of
425
+ audited.
357
426
 
358
427
  Some pages are meant to exist in one locale and not another — a
359
428
  jurisdiction-specific legal notice, a post written for one market. A site has
@@ -367,6 +436,17 @@ goflag https://example.com --ignore-holes /legal --ignore-holes "/blog/**"
367
436
  Suppressed gaps are counted in `diagnostics.ignoredHoles`, so a quiet report
368
437
  never means "nothing was wrong" by accident.
369
438
 
439
+ The opposite failure is quieter still. A cell in the matrix can be filled by an
440
+ `hreflang` on another page and by nothing else — not crawled, not listed in your
441
+ sitemap — and a filled cell reads as a translation that exists. A page
442
+ advertising a translation the site does not serve therefore plugs the very gap
443
+ it should reveal. Those cells are counted in
444
+ `diagnostics.unverifiedAlternates`, and goflag deliberately does **not** act on
445
+ them: refusing to believe an alternate the sitemap omits would invent holes on
446
+ every site using `@goflag/next`'s `sitemap: false` on purpose. Read that number
447
+ next to `missingTranslations` — a `0` beside a non-zero count is the one place
448
+ this report can be quietly wrong.
449
+
370
450
  ## In CI
371
451
 
372
452
  Two moments are worth auditing, and they answer different questions.
@@ -398,7 +478,7 @@ Three rules, whichever you use:
398
478
  stages: [build, deploy, audit]
399
479
 
400
480
  variables:
401
- GOFLAG_VERSION: "0.1.4"
481
+ GOFLAG_VERSION: "0.2.8"
402
482
 
403
483
  # Before the merge: build the branch, let goflag boot it, audit that.
404
484
  seo:mr:
@@ -412,6 +492,7 @@ seo:mr:
412
492
  - npx --yes "@goflag/cli@$GOFLAG_VERSION" http://localhost:3000
413
493
  --start "pnpm start" --static --no-external
414
494
  --baseline .goflag/baseline.json --regressions-only --max-debt 41
495
+ --report goflag-report.json
415
496
  artifacts:
416
497
  when: always
417
498
  paths: [goflag-report.json]
@@ -449,9 +530,10 @@ jobs:
449
530
  - run: corepack enable && pnpm install --frozen-lockfile
450
531
  - run: pnpm build
451
532
  - run: |
452
- npx --yes @goflag/cli@0.1.4 http://localhost:3000 \
533
+ npx --yes @goflag/cli@0.2.8 http://localhost:3000 \
453
534
  --start "pnpm start" --static --no-external \
454
- --baseline .goflag/baseline.json --regressions-only --max-debt 41
535
+ --baseline .goflag/baseline.json --regressions-only --max-debt 41 \
536
+ --report goflag-report.json
455
537
  - uses: actions/upload-artifact@v4
456
538
  if: always()
457
539
  with:
@@ -484,7 +566,8 @@ you what it accepted, and that number belongs in the commit message.
484
566
  import { runAudit } from "@goflag/cli";
485
567
 
486
568
  const report = await runAudit("https://example.com", { depth: 2 });
487
- console.log(report.summary); // { brokenLinks, missingTranslations, seoIssues, verdict }
569
+ console.log(report.summary);
570
+ // { brokenLinks, missingTranslations, seoIssues, siteIssues, unreachablePages, verdict }
488
571
  ```
489
572
 
490
573
  `runAudit` returns the same `GoflagReport` object the CLI emits.
@@ -543,9 +626,11 @@ pnpm typecheck # tsc --noEmit
543
626
  pnpm format # prettier
544
627
  ```
545
628
 
546
- `lint`, `format` and `format:check` run once over the whole repository; the
547
- rest fan out across workspace packages with `pnpm -r`. To work on one package,
548
- filter: `pnpm --filter @goflag/cli test`.
629
+ `format` and `format:check` run once over the whole repository. `lint` runs
630
+ eslint once at the root and then fans out to any package that defines its own
631
+ `lint` script — `apps/website` does, with the Next config — and the rest fan out
632
+ across workspace packages with `pnpm -r`. To work on one package, filter:
633
+ `pnpm --filter @goflag/cli test`.
549
634
 
550
635
  The engine is framework-agnostic and lives in [`packages/cli/src/lib/core/`](packages/cli/src/lib/core) (crawl, fetch/extract, link audit, i18n) and [`packages/cli/src/lib/rules/`](packages/cli/src/lib/rules) (SEO checks). The CLI shell — orchestration, the `GoflagReport` schema, and rendering — lives in [`packages/cli/src/report/`](packages/cli/src/report) and [`packages/cli/src/cli.ts`](packages/cli/src/cli.ts).
551
636