@goflag/cli 0.1.4 → 0.2.0

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
@@ -1,12 +1,17 @@
1
1
  # Goflag
2
2
 
3
3
  > **A lean CLI that flags the site problems humans can't catch at scale.**
4
- > Point it at a URL and get three things: broken links, missing translation pages, and missing/misconfigured SEO metadata. JSON-first.
4
+ > Point it at a URL and get four things: broken links, missing translation pages, a robots.txt that contradicts your pages, and missing or misconfigured SEO metadata. JSON-first.
5
5
 
6
- [![node](https://img.shields.io/badge/node-%3E%3D20.11-339933?logo=nodedotjs&logoColor=white)](./package.json)
6
+ [![node](https://img.shields.io/badge/node-%3E%3D22-339933?logo=nodedotjs&logoColor=white)](./package.json)
7
7
  [![license: MIT](https://img.shields.io/badge/license-MIT-green)](#license)
8
8
 
9
- Goflag crawls a site once and runs three focused static checks over it. It is deliberately small: no dashboard, no config system, no social-preview gallery. Just the findings, as a machine-readable report you can pipe, diff, or gate CI on.
9
+ Goflag crawls a site once and judges what it found. It is deliberately small: no dashboard, no config system, no social-preview gallery. Just the findings, as a machine-readable report you can pipe, diff, or gate CI on.
10
+
11
+ It is built for the mistakes that are invisible while browsing and expensive in
12
+ search: a `hreflang` that points at a 404, a canonical that silently
13
+ de-indexes a page, a `robots.txt` that contradicts every `<meta name="robots">`
14
+ on the site. A human cannot check those on 400 pages. That is the whole job.
10
15
 
11
16
  ## What it checks
12
17
 
@@ -17,37 +22,88 @@ Goflag crawls a site once and runs three focused static checks over it. It is de
17
22
 
18
23
  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`.
19
24
 
20
- SPA support is built in: pages that look client-rendered are re-rendered in headless Chromium automatically (pass `--static` to skip it).
25
+ **Client-rendered pages.** By default, a page whose `<head>` looks empty — no
26
+ title, no description, no canonical, no OG, no JSON-LD — is re-rendered in
27
+ headless Chromium before being judged, so a SPA is not reported as missing
28
+ everything. `--static` turns that off, including the detection: with it, a
29
+ client-rendered page is judged on its unhydrated shell.
21
30
 
22
- ## Repository layout
31
+ That direction is deliberate. Static mode over-reports rather than under-reports
32
+ — the metadata is genuinely absent from the HTML a crawler receives — so it
33
+ fails loudly rather than passing quietly. It is also why `--static` is the right
34
+ default in CI: it needs no browser, and it cannot mistake a broken page for a
35
+ fine one.
23
36
 
24
- ```
25
- packages/cli/ @goflag/cli — the CLI (installs the `goflag` command)
26
- tools/name-holder/ the bare `goflag` name on npm, a signpost to @goflag/cli
37
+ ## Install
38
+
39
+ Requires Node `>=22`.
40
+
41
+ ```sh
42
+ npx @goflag/cli https://example.com # one-off, nothing installed
43
+ pnpm add -D @goflag/cli # then: pnpm goflag https://example.com
27
44
  ```
28
45
 
29
- That is the whole tree. `@goflag/next` (the Next.js library) and the
30
- documentation site are planned but deliberately absent: the workspace globs
31
- (`packages/*`, `apps/*`) are ready for them, and empty stubs for things that do
32
- not exist are how a repository accumulates code nobody calls.
46
+ Install it as a dev dependency once you run it more than once — in a script, in
47
+ CI, in a git hook. `npx` is for trying it.
33
48
 
34
- When they land, they must not import from the CLI: the two products stay
35
- independently useful, and an ESLint rule enforces it rather than trusting
36
- memory.
49
+ The package is `@goflag/cli`; the command it installs is `goflag`. Everything
50
+ under the brand lives in the `@goflag` scope; the bare `goflag` name on npm is a
51
+ deprecated signpost pointing here and carries no code.
52
+
53
+ Headless rendering needs Chromium, which is **not** installed with goflag —
54
+ `playwright` is an optional peer dependency. Add it only if you audit
55
+ client-rendered pages without `--static`:
56
+
57
+ ```sh
58
+ pnpm add -D playwright && pnpm exec playwright install chromium
59
+ ```
60
+
61
+ ## Getting started
37
62
 
38
- ## Quick start
63
+ Five steps, in this order. Each one is explained in full further down; this is
64
+ the sequence.
39
65
 
40
- Requires Node `>=20.11`.
66
+ **1. Look at your site.** No flags, no gate — find out what is there.
41
67
 
42
68
  ```sh
43
69
  npx @goflag/cli https://example.com
44
70
  ```
45
71
 
46
- The package is `@goflag/cli`; the command it installs is `goflag`. Everything
47
- under the brand lives in the `@goflag` scope. The bare `goflag` name on npm is
48
- claimed as a deprecated signpost pointing here — it carries no code, so install
49
- the scoped package. The reasoning is in
50
- [docs/spec-and-lib-plan.md](docs/spec-and-lib-plan.md#une-marque-des-outils-nommés).
72
+ Expect it to be red. Almost every site that has never been audited has a
73
+ backlog, and that is not a reason to fix everything before continuing.
74
+
75
+ **2. Decide what is real.** Some findings are deliberate — a legal page that
76
+ exists in one jurisdiction, a route you chose not to translate. Tell goflag,
77
+ rather than living with noise:
78
+
79
+ ```sh
80
+ npx @goflag/cli https://example.com --ignore-holes /legal
81
+ ```
82
+
83
+ **3. Capture a baseline.** This freezes today's findings as "known", so the gate
84
+ can ask _did this change make it worse?_ instead of _is this site perfect?_
85
+
86
+ ```sh
87
+ npx @goflag/cli https://example.com --static --no-external \
88
+ --baseline .goflag/baseline.json --update-baseline
89
+ ```
90
+
91
+ It prints how many findings it grandfathered and the `--max-debt` to set next.
92
+ Commit `.goflag/baseline.json`.
93
+
94
+ **4. Gate CI on regressions.** See [In CI](#in-ci) for a job you can copy.
95
+
96
+ ```sh
97
+ goflag https://example.com --static --no-external \
98
+ --baseline .goflag/baseline.json --regressions-only --max-debt 41
99
+ ```
100
+
101
+ **5. Lower the ratchet.** Fix a finding, drop `--max-debt` by one, commit both.
102
+ This is the only part that makes the backlog shrink; without it a baseline
103
+ fossilises behind a passing build.
104
+
105
+ Steps 3 to 5 are the whole method. The rest of this README is why each flag
106
+ behaves the way it does.
51
107
 
52
108
  You get a coloured terminal report and an exit code:
53
109
 
@@ -55,6 +111,11 @@ You get a coloured terminal report and an exit code:
55
111
  - `1` — findings present (yellow/red flag) — use this as a CI gate
56
112
  - `2` — fatal error (bad URL, unexpected failure)
57
113
 
114
+ ## Using it
115
+
116
+ Everything below is reference: what each flag does and why it behaves that way.
117
+ The five steps above are the path; this is the map.
118
+
58
119
  ### JSON is the source of truth
59
120
 
60
121
  ```sh
@@ -77,6 +138,12 @@ The terminal view is just a render of that JSON.
77
138
  --ignore-holes <glob> Route deliberately not translated everywhere
78
139
  (repeatable).
79
140
  --no-sitemap Do not discover the sitemap; crawl from <url> only.
141
+ --profile <name> Policy overlay on the rule set: default, strict,
142
+ spec-only, marketing.
143
+ --conformance Report every rule's status on every page, not just
144
+ the violations.
145
+ --advisories Attach the prose rules — the judgment calls goflag
146
+ refuses to fake — with the facts to judge them by.
80
147
  --fail-on <level> Exit 1 at or above this severity: warning (default),
81
148
  error, or never.
82
149
  --regressions-only Weaken the gate: fail only on NEW findings relative
@@ -101,6 +168,100 @@ The terminal view is just a render of that JSON.
101
168
 
102
169
  Headless mode needs Chromium. `playwright` is an optional peer dependency; if it's missing, install it with `npx playwright install chromium` (or just run with `--static`).
103
170
 
171
+ ### Profiles: how much each rule matters to you
172
+
173
+ Every rule records how authoritative it is — whether a published spec requires
174
+ it, a vendor documents it, or it is widely-repeated folklore. That is a fact
175
+ about the rule, and goflag never fudges it. What your build should _do_ about
176
+ each one is a separate question, and that is what `--profile` answers:
177
+
178
+ ```sh
179
+ goflag https://example.com --profile spec-only # only what a spec backs
180
+ goflag https://example.com --profile strict # spec-backed rules fail the build
181
+ goflag https://example.com --profile marketing # metadata gaps are errors
182
+ ```
183
+
184
+ | Profile | What it does |
185
+ | ----------- | ------------------------------------------------------------------------------ |
186
+ | `default` | Each rule's own severity. No overlay. |
187
+ | `strict` | Every spec-backed rule becomes an `error`. Heuristics stay warnings. |
188
+ | `spec-only` | Heuristic rules (title/description length) are switched off entirely. |
189
+ | `marketing` | The snippet and unfurl metadata — description, `og:title`, `og:image` — error. |
190
+
191
+ A profile only changes how loudly a rule fires and whether it runs at all. It
192
+ never changes what a rule observes, and it never rewrites a rule's stated
193
+ authority — so `strict` can make a finding fail your build, but it cannot turn
194
+ folklore into a spec requirement.
195
+
196
+ The report records which profile produced it (`"profile": "strict"`), and the
197
+ terminal output names any non-default one. A run under `spec-only` reporting
198
+ zero issues means "nothing a spec backs is wrong here", not "nothing is wrong".
199
+
200
+ That record is load-bearing: comparing a run against a baseline captured under
201
+ a different profile still works, but "0 new findings" no longer means what it
202
+ appears to, so goflag says so rather than letting the number speak for itself:
203
+
204
+ ```
205
+ REGRESSION GATE 0 new · 19 known findings NOT gating this build
206
+ baseline https://example.com — taken 2026-08-06 (today)
207
+ note: baseline was captured under profile `strict`, this run used `spec-only` — the two are not like-for-like.
208
+ ```
209
+
210
+ It is a warning, never a gate — a `--profile spec-only` investigation against a
211
+ `strict` baseline is a legitimate thing to do.
212
+
213
+ ### Where do we stand? (`--conformance`)
214
+
215
+ A list of violations cannot tell you the difference between a rule that
216
+ passes everywhere and a rule that never applied to a single page. Both look
217
+ like silence. `--conformance` reports every rule's status on every page:
218
+
219
+ ```sh
220
+ goflag https://example.com --conformance
221
+ ```
222
+
223
+ ```
224
+ Conformance
225
+ 6 pages × every rule
226
+ canonical.absolute 6 fail [vendor-spec]
227
+ canonical.missing 6 pass [vendor-spec]
228
+ description.length 6 n/a [heuristic]
229
+ title.missing 6 pass [spec-required]
230
+ ```
231
+
232
+ The terminal shows the tally; `--json` carries the full rule × page grid,
233
+ with each rule's rigor and sources in a legend rather than repeated per
234
+ cell. `pass + fail + warn + n/a + crashed` always equals the page count, so
235
+ the coverage claim is checkable.
236
+
237
+ ### The questions goflag will not answer for you (`--advisories`)
238
+
239
+ Whether a title _describes_ the page is not a thing a linter can decide.
240
+ goflag could fake it — count words, match boilerplate, emit a confident
241
+ verdict — and the result would be unfalsifiable noise. So it states the
242
+ question, cites what makes it a real requirement, attaches the observed
243
+ facts, and stops:
244
+
245
+ ```sh
246
+ goflag https://example.com --advisories --json
247
+ ```
248
+
249
+ ```json
250
+ {
251
+ "ruleId": "description.accurate",
252
+ "prose": "Does the description accurately summarize this page's content, and is it written for this page rather than copied across the site?",
253
+ "rigor": "guideline",
254
+ "sources": ["google-snippet", "moz-meta-description"],
255
+ "evidence": { "meta.description": { "value": "…", "origin": { "kind": "meta" } } },
256
+ "verdict": "needs-judgment"
257
+ }
258
+ ```
259
+
260
+ Advisories are asked only where the subject exists — no question about a
261
+ description on a page that has none, because `description.missing` already
262
+ says that. They never count toward the summary, the verdict, or the exit
263
+ code: nobody has judged them yet.
264
+
104
265
  ### Gate on regressions, not on perfection
105
266
 
106
267
  A plain run fails on any finding, which is unusable on a site that is not clean
@@ -206,6 +367,117 @@ goflag https://example.com --ignore-holes /legal --ignore-holes "/blog/**"
206
367
  Suppressed gaps are counted in `diagnostics.ignoredHoles`, so a quiet report
207
368
  never means "nothing was wrong" by accident.
208
369
 
370
+ ## In CI
371
+
372
+ Two moments are worth auditing, and they answer different questions.
373
+
374
+ | When | Against | Answers |
375
+ | -------------------- | ------------------------------------------- | ---------------------------------------- |
376
+ | On the merge request | the branch's own build, booted by `--start` | does **this change** regress? |
377
+ | After deploying | the running environment | is what is **actually serving** correct? |
378
+
379
+ Neither replaces the other. Only the deployed run sees what the environment
380
+ injects — the real base URL, whatever the proxy serves for `robots.txt`,
381
+ redirects. A canonical broken by a misconfigured environment variable is
382
+ invisible in a local build.
383
+
384
+ Three rules, whichever you use:
385
+
386
+ - **Pin the version.** A floating one turns someone else's release into a red
387
+ pipeline on a commit that touched nothing, and the job is only worth having if
388
+ red means "you broke something".
389
+ - **Commit the baseline.** Grandfathering a finding then shows up in a diff
390
+ somebody reviews, instead of vanishing into a cache.
391
+ - **Build the same site the baseline was captured against.** Frameworks bake
392
+ build-time environment into the output; a build that differs produces
393
+ differences that are real in the report and meaningless as findings.
394
+
395
+ ### GitLab CI
396
+
397
+ ```yaml
398
+ stages: [build, deploy, audit]
399
+
400
+ variables:
401
+ GOFLAG_VERSION: "0.1.4"
402
+
403
+ # Before the merge: build the branch, let goflag boot it, audit that.
404
+ seo:mr:
405
+ stage: audit
406
+ image: node:24-alpine
407
+ rules:
408
+ - if: $CI_MERGE_REQUEST_IID
409
+ script:
410
+ - corepack enable && pnpm install --frozen-lockfile
411
+ - pnpm build
412
+ - npx --yes "@goflag/cli@$GOFLAG_VERSION" http://localhost:3000
413
+ --start "pnpm start" --static --no-external
414
+ --baseline .goflag/baseline.json --regressions-only --max-debt 41
415
+ artifacts:
416
+ when: always
417
+ paths: [goflag-report.json]
418
+
419
+ # After the deploy: audit what is serving.
420
+ seo:
421
+ stage: audit
422
+ image: node:24-alpine
423
+ needs: [{ job: deploy, artifacts: false }]
424
+ rules:
425
+ - if: $CI_COMMIT_BRANCH == "develop"
426
+ script:
427
+ - npx --yes "@goflag/cli@$GOFLAG_VERSION" https://develop.example.com
428
+ --static --no-external
429
+ --baseline .goflag/baseline.json --regressions-only --max-debt 41
430
+ --report goflag-report.json
431
+ artifacts:
432
+ when: always
433
+ paths: [goflag-report.json]
434
+ ```
435
+
436
+ ### GitHub Actions
437
+
438
+ ```yaml
439
+ name: seo
440
+ on: [pull_request]
441
+
442
+ jobs:
443
+ audit:
444
+ runs-on: ubuntu-latest
445
+ steps:
446
+ - uses: actions/checkout@v4
447
+ - uses: actions/setup-node@v4
448
+ with: { node-version: 24 }
449
+ - run: corepack enable && pnpm install --frozen-lockfile
450
+ - run: pnpm build
451
+ - run: |
452
+ npx --yes @goflag/cli@0.1.4 http://localhost:3000 \
453
+ --start "pnpm start" --static --no-external \
454
+ --baseline .goflag/baseline.json --regressions-only --max-debt 41
455
+ - uses: actions/upload-artifact@v4
456
+ if: always()
457
+ with:
458
+ name: goflag-report
459
+ path: goflag-report.json
460
+ ```
461
+
462
+ ### Why `--static --no-external` in CI
463
+
464
+ `--static` needs no browser, so the job runs on a plain Node image and cannot
465
+ be slowed by a Chromium download. `--no-external` skips off-origin links, which
466
+ are the ones you cannot fix and whose failures are somebody else's outage — a
467
+ gate that goes red because a third party is down teaches people to ignore it.
468
+
469
+ Audit external links on a schedule instead, where nobody is waiting.
470
+
471
+ ### Reading a red build
472
+
473
+ The report artefact is the thing to open. It is the same JSON as `--json`, and
474
+ it is kept whether the job passed or failed, because it is most wanted when it
475
+ is red.
476
+
477
+ If the finding is one you have decided to accept, refresh the baseline with the
478
+ same command minus `--regressions-only`, plus `--update-baseline`. It will tell
479
+ you what it accepted, and that number belongs in the commit message.
480
+
209
481
  ## Programmatic API
210
482
 
211
483
  ```ts
@@ -217,6 +489,22 @@ console.log(report.summary); // { brokenLinks, missingTranslations, seoIssues, v
217
489
 
218
490
  `runAudit` returns the same `GoflagReport` object the CLI emits.
219
491
 
492
+ ## Repository layout
493
+
494
+ ```
495
+ packages/cli/ @goflag/cli — the CLI (installs the `goflag` command)
496
+ tools/name-holder/ the bare `goflag` name on npm, a signpost to @goflag/cli
497
+ ```
498
+
499
+ That is the whole tree. `@goflag/next` (the Next.js library) and the
500
+ documentation site are planned but deliberately absent: the workspace globs
501
+ (`packages/*`, `apps/*`) are ready for them, and empty stubs for things that do
502
+ not exist are how a repository accumulates code nobody calls.
503
+
504
+ When they land, they must not import from the CLI: the two products stay
505
+ independently useful, and an ESLint rule enforces it rather than trusting
506
+ memory.
507
+
220
508
  ## Develop locally
221
509
 
222
510
  Requires pnpm `11.18.0` (pinned via `packageManager` — `corepack enable` picks