@goflag/cli 0.2.10 → 0.2.12

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/dist/index.d.ts CHANGED
@@ -379,6 +379,43 @@ interface ExtractionAsset {
379
379
  */
380
380
  sizes?: ExtractionAssetSize[];
381
381
  }
382
+ /** One tag, named the way the page named it. */
383
+ interface ExtractionHydratedMeta {
384
+ name?: string;
385
+ property?: string;
386
+ httpEquiv?: string;
387
+ content?: string;
388
+ }
389
+ /** One link, named the way the page named it. */
390
+ interface ExtractionHydratedLink {
391
+ rel: string;
392
+ href?: string;
393
+ hreflang?: string;
394
+ }
395
+ /**
396
+ * What client JavaScript added to, or took from, the `<head>`.
397
+ *
398
+ * The gap between the two passes, at tag granularity — not per `Fact`, which
399
+ * carries which *tag* produced a value and never which *pass*. A value present
400
+ * here is a value the browser shows and a non-JS crawler never sees: the
401
+ * failure mode every `og.*` rule is blind to, because each of them judges the
402
+ * declaration it is given and this is about which declaration that was.
403
+ *
404
+ * **Absent on most runs, and that is not "nothing changed".** The delta only
405
+ * exists where both bodies do: the auto-escalation path, where a static fetch
406
+ * looked empty and a headless pass followed. A `--static` run never renders,
407
+ * and an explicit `--headless` run keeps no static body to compare against.
408
+ */
409
+ interface ExtractionHydration {
410
+ titleChanged: boolean;
411
+ htmlLangChanged: boolean;
412
+ injectedMetas: ExtractionHydratedMeta[];
413
+ removedMetas: ExtractionHydratedMeta[];
414
+ injectedLinks: ExtractionHydratedLink[];
415
+ removedLinks: ExtractionHydratedLink[];
416
+ /** JSON-LD blocks that exist only after hydration. */
417
+ jsonLdBlocksAdded: number;
418
+ }
382
419
  /**
383
420
  * The per-page observation. Everything a page rule (or an agent judging a
384
421
  * prose rule) may read; nothing else.
@@ -402,6 +439,11 @@ interface Extraction {
402
439
  * manifest gets.
403
440
  */
404
441
  assets?: Record<string, ExtractionAsset>;
442
+ /**
443
+ * What hydration changed in the `<head>`, when both passes ran and can be
444
+ * compared. Absent means "not established" — never "nothing moved".
445
+ */
446
+ hydration?: ExtractionHydration;
405
447
  }
406
448
 
407
449
  /**
@@ -517,7 +559,17 @@ interface AdvisoryFinding {
517
559
  ruleId: string;
518
560
  kind: "prose";
519
561
  prose: string;
520
- rigor: Rigor;
562
+ /**
563
+ * `null` where no document supports the question.
564
+ *
565
+ * Every page prose rule carries one. The cross-page ones (`./site-prose.ts`)
566
+ * may not: `hreflang.sitemap-mismatch` asks about a disagreement no
567
+ * specification addresses, and inventing a rigor for it would put back the
568
+ * dishonesty that moving it out of the verdict-bearing registry removed.
569
+ * An agent reads this to weight the question; `null` says "weight it on the
570
+ * evidence alone".
571
+ */
572
+ rigor: Rigor | null;
521
573
  sources: string[];
522
574
  /**
523
575
  * Extraction path → the observed value there, one entry per declared
@@ -864,6 +916,26 @@ interface GoflagReport {
864
916
  * touch the summary counts, the verdict, or the exit code.
865
917
  */
866
918
  advisories?: ReportAdvisory[];
919
+ /**
920
+ * What each page declared in its `<head>`, as the rules saw it. Present
921
+ * only when asked for (`AuditOptions.extractions`, which `goflag preview`
922
+ * sets).
923
+ *
924
+ * Every other section of this report is a *judgment*: a rule fired, or it
925
+ * did not. This one is the observation the judgments were made from, and it
926
+ * is here because a share card cannot be drawn from a violations list — a
927
+ * page that passes every `og.*` rule still has a title, an image and a
928
+ * description that somebody wants to look at, and none of them appear
929
+ * anywhere else in this file.
930
+ *
931
+ * Keyed by nothing: each entry carries its own `http.finalUrl`, which is the
932
+ * same string `ReportPage.url` and `SeoIssue.pageUrl` use.
933
+ *
934
+ * Covers fewer pages than `pages`, for the same reason `conformance` does:
935
+ * only healthy HTML documents reach the rule layer, so an unreachable page,
936
+ * a non-HTML resource and a canonical-declared duplicate have no entry here.
937
+ */
938
+ extractions?: Extraction[];
867
939
  /** Comparison against a stored baseline, when `--baseline` was given. */
868
940
  diff?: ReportDiff;
869
941
  diagnostics: {
@@ -1044,6 +1116,20 @@ interface AuditOptions {
1044
1116
  * carry no verdict and never affect the gate.
1045
1117
  */
1046
1118
  advisories?: boolean;
1119
+ /**
1120
+ * Emit each page's extraction — what its `<head>` declared, as the rules
1121
+ * read it. Off by default: it is the observation, not a finding, and on a
1122
+ * large crawl it is the biggest section of the report.
1123
+ *
1124
+ * `goflag preview` sets it, because a share card is drawn from the
1125
+ * declarations and not from the verdicts about them.
1126
+ *
1127
+ * Unlike `conformance` and `advisories`, this section stops at the report:
1128
+ * `summarize()` does not carry it — a summary of an observation is the
1129
+ * observation with the useful parts removed — and no terminal renderer
1130
+ * prints it. Its one reader is `renderPreview`.
1131
+ */
1132
+ extractions?: boolean;
1047
1133
  }
1048
1134
  /**
1049
1135
  * Routes present in some locales but absent in others. A "hole" is exactly
@@ -1215,6 +1301,50 @@ interface RenderOptions {
1215
1301
  }
1216
1302
  declare function renderSummaryTerminal(summary: GoflagSummary, options?: RenderOptions): string;
1217
1303
 
1304
+ /**
1305
+ * The share-card preview — one self-contained HTML document.
1306
+ *
1307
+ * `renderPreview` is a pure function of a `GoflagReport`, exactly like the
1308
+ * three terminal renderers beside it. It reads `report.extractions`, which is
1309
+ * the only section that carries what a page *declared* rather than what a rule
1310
+ * *decided*: a page can pass every `og.*` rule and still unfurl badly, and no
1311
+ * violations list can show that.
1312
+ *
1313
+ * Two things this file refuses to do, and they are the point.
1314
+ *
1315
+ * **It draws no geometry it cannot source.** Every surface carries the same
1316
+ * rigor scale the rule catalogue uses, because the surfaces are not equally
1317
+ * documented: Facebook, LinkedIn and WhatsApp publish real numbers; Slack calls
1318
+ * its own rendering a "micro-approximation"; Discord publishes nothing at all;
1319
+ * X's card documentation is not reachable, and its shape changed twice since
1320
+ * 2023. A preview that renders all seven identically states seven things with
1321
+ * one confidence, and six of them are not earned.
1322
+ *
1323
+ * **It escapes everything.** The input is a site under audit, which is to say
1324
+ * untrusted text, and the output is a file a human opens in a browser. Values
1325
+ * go through `esc`; URLs that are not `http(s)` never become an `src`.
1326
+ *
1327
+ * Opening the document asks for the image URLs the pages declared, which is
1328
+ * what makes it a preview rather than a table. Those URLs are the site's own
1329
+ * choice and need not point at the audited origin, so the document ships a CSP
1330
+ * that allows images and nothing else — no script, no frame, no fetch — and
1331
+ * every image is `referrerpolicy="no-referrer"`. Rendering itself never touches
1332
+ * the network.
1333
+ */
1334
+
1335
+ interface RenderPreviewOptions {
1336
+ /** Document `<title>`. Defaults to the audited origin. */
1337
+ title?: string;
1338
+ }
1339
+ /**
1340
+ * Render the whole preview.
1341
+ *
1342
+ * A report without `extractions` renders a document that says so rather than an
1343
+ * empty one: the caller asked for a preview and got a report that cannot make
1344
+ * one, and that is a fact about the run, not an error to swallow.
1345
+ */
1346
+ declare function renderPreview(report: GoflagReport, options?: RenderPreviewOptions): string;
1347
+
1218
1348
  /**
1219
1349
  * Stable finding fingerprints.
1220
1350
  *
@@ -1318,4 +1448,4 @@ declare class Logger {
1318
1448
  private truncate;
1319
1449
  }
1320
1450
 
1321
- export { type AuditOptions, type AuditPhase, type BrokenLink, type GoflagReport, type GoflagSummary, type LogMode, Logger, type LoggerOptions, type ProgressEvent, type ReportPage, type ReportReciprocityIssue, type RollupLink, type RollupReciprocity, type RollupSeo, SAMPLE_LIMIT, type SeoIssue, type TranslationHole, type UnreachablePage, type Verdict, deriveTranslationHoles, exitCode, fingerprint, renderSummaryTerminal, renderTerminal, routeKey, runAudit, summarize, targetKey };
1451
+ export { type AuditOptions, type AuditPhase, type BrokenLink, type GoflagReport, type GoflagSummary, type LogMode, Logger, type LoggerOptions, type ProgressEvent, type RenderPreviewOptions, type ReportPage, type ReportReciprocityIssue, type RollupLink, type RollupReciprocity, type RollupSeo, SAMPLE_LIMIT, type SeoIssue, type TranslationHole, type UnreachablePage, type Verdict, deriveTranslationHoles, exitCode, fingerprint, renderPreview, renderSummaryTerminal, renderTerminal, routeKey, runAudit, summarize, targetKey };
package/dist/index.js CHANGED
@@ -5,19 +5,21 @@ import {
5
5
  deriveTranslationHoles,
6
6
  exitCode,
7
7
  fingerprint,
8
+ renderPreview,
8
9
  renderSummaryTerminal,
9
10
  renderTerminal,
10
11
  routeKey,
11
12
  runAudit,
12
13
  summarize,
13
14
  targetKey
14
- } from "./chunk-DKGBLJ4H.js";
15
+ } from "./chunk-QKXUKHXB.js";
15
16
  export {
16
17
  Logger,
17
18
  SAMPLE_LIMIT,
18
19
  deriveTranslationHoles,
19
20
  exitCode,
20
21
  fingerprint,
22
+ renderPreview,
21
23
  renderSummaryTerminal,
22
24
  renderTerminal,
23
25
  routeKey,
package/flags.json CHANGED
@@ -241,7 +241,7 @@
241
241
  "default": null,
242
242
  "requires": null,
243
243
  "group": "output",
244
- "description": "Roll findings up (dedup by link/rule/code). Pairs with --json for a compact, agent-friendly payload."
244
+ "description": "Roll findings up (dedup by link/rule/code). Pairs with --json for a compact, agent-friendly payload. Not available with --baseline: there the diff is the answer, and --report writes the full report."
245
245
  },
246
246
  {
247
247
  "name": "--timeout",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goflag/cli",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "type": "module",
5
5
  "description": "Audits a site for broken links, missing translation pages, a robots.txt that contradicts your pages, and missing or misconfigured SEO metadata. JSON-first.",
6
6
  "keywords": [
@@ -66,7 +66,7 @@
66
66
  "generate:help-fixture": "tsx scripts/generate-help-fixture.ts",
67
67
  "prepack": "node ../../scripts/stage-package-docs.mjs",
68
68
  "prepublishOnly": "pnpm build",
69
- "release": "commit-and-tag-version --path . --skip.tag"
69
+ "release": "commit-and-tag-version --path . --skip.tag --tag-prefix v"
70
70
  },
71
71
  "dependencies": {
72
72
  "cheerio": "^1.2.0"
package/rules.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "counts": {
3
- "page": 23,
4
- "site": 26,
5
- "prose": 4
3
+ "page": 25,
4
+ "site": 28,
5
+ "prose": 5
6
6
  },
7
7
  "sources": {
8
8
  "apple-web-apps": {
@@ -319,23 +319,39 @@
319
319
  "language": "tsx"
320
320
  }
321
321
  },
322
+ {
323
+ "id": "hreflang.cluster-incomplete",
324
+ "scope": "site",
325
+ "severity": "warning",
326
+ "kind": "boolean",
327
+ "summary": "Every locale the sitemap publishes must appear in the route's `hreflang` cluster",
328
+ "rigor": "vendor-spec",
329
+ "sources": [
330
+ "google-hreflang"
331
+ ]
332
+ },
322
333
  {
323
334
  "id": "hreflang.missing",
324
335
  "scope": "site",
325
336
  "severity": "error",
326
337
  "kind": "boolean",
327
338
  "summary": "Pages on a multilingual site must advertise their locale alternates",
328
- "rigor": null,
329
- "sources": []
339
+ "rigor": "vendor-spec",
340
+ "sources": [
341
+ "google-hreflang",
342
+ "whatwg-html-link-types"
343
+ ]
330
344
  },
331
345
  {
332
346
  "id": "hreflang.sitemap-mismatch",
333
- "scope": "site",
334
- "severity": "warning",
335
- "kind": "boolean",
336
- "summary": "`<head>` alternates and sitemap locale coverage must agree",
347
+ "scope": "prose",
348
+ "severity": null,
349
+ "kind": "prose",
350
+ "summary": "`<head>` alternates advertise a locale the sitemap does not list",
351
+ "why": "The two are derived from one intent by different code paths, so a disagreement means one of them is wrong. No specification says which: the declaration methods are equivalent, and a page can legitimately be cross-linked and kept out of the sitemap.",
337
352
  "rigor": null,
338
- "sources": []
353
+ "sources": [],
354
+ "prose": "Your `<head>` advertises a translation your sitemap does not list. Is the sitemap missing an entry, or is the alternate pointing at a page you did not mean to publish?"
339
355
  },
340
356
  {
341
357
  "id": "icons.apple-touch.missing",
@@ -563,6 +579,34 @@
563
579
  "language": "tsx"
564
580
  }
565
581
  },
582
+ {
583
+ "id": "og.image.alt.caption",
584
+ "scope": "page",
585
+ "category": "opengraph",
586
+ "severity": "info",
587
+ "kind": "boolean",
588
+ "summary": "`og:image:alt` describes the image, it does not repeat the title",
589
+ "why": "The protocol asks for a description of what is in the image, and says in the same breath that it is not a caption. A copy of the title is the one value that is always available and never describes anything: a reader who cannot see the card is handed the same words twice and learns nothing about the picture. It also satisfies `og.image.alt` forever, which is why it needs an id of its own.",
590
+ "rigor": "guideline",
591
+ "sources": [
592
+ "ogp"
593
+ ],
594
+ "reads": [
595
+ "openGraph.images",
596
+ "openGraph.title",
597
+ "document.title"
598
+ ],
599
+ "relates": [
600
+ "og.image.alt",
601
+ "og.image.representative"
602
+ ],
603
+ "expected": "an `og:image:alt` that says something the title does not",
604
+ "fix": {
605
+ "title": "Say what the card shows, not what the page is called",
606
+ "snippet": "// The card is a title on a background. The alt says so — once, from a\n// template, so every page gets one and none of them is the title alone.\nconst ogAlt = (title: string) => `The title “${title}” on a dark preview card.`;",
607
+ "language": "ts"
608
+ }
609
+ },
566
610
  {
567
611
  "id": "og.image.dimensions",
568
612
  "scope": "page",
@@ -695,6 +739,35 @@
695
739
  ],
696
740
  "prose": "Does the og:image represent what this page is about, rather than being a site-wide default, and does its subject survive being cropped to the 1.91:1 aspect ratio consumers render?"
697
741
  },
742
+ {
743
+ "id": "og.image.sizes-mismatch",
744
+ "scope": "page",
745
+ "category": "opengraph",
746
+ "severity": "warning",
747
+ "kind": "boolean",
748
+ "summary": "`og:image:width` and `og:image:height` must be the image's real size",
749
+ "why": "The declared size is what a crawler lays the card out with before it has the file, which is the whole reason `og.image.dimensions` asks for it. A wrong one is therefore worse than none: the crawler reserves a shape the image does not have, and the card is letterboxed or cropped on the first share — the one that matters most.\n\nIt is also the declaration that blinds every other check. `og.image.ratio` reads these two numbers and refuses to fetch, so an invented 1200×630 scores 1.9 and passes over an image that is square. Found on a site whose cover art is 1024×1024 and one of whose covers is a 337-byte 1×1 placeholder, all three declared 1200×630 by a library that had never looked at them.",
750
+ "rigor": "guideline",
751
+ "sources": [
752
+ "ogp",
753
+ "meta-og-sharing"
754
+ ],
755
+ "reads": [
756
+ "openGraph.images",
757
+ "assets"
758
+ ],
759
+ "relates": [
760
+ "og.image.dimensions",
761
+ "og.image.ratio",
762
+ "og.image.reachable"
763
+ ],
764
+ "expected": "declared dimensions equal to the file's own",
765
+ "fix": {
766
+ "title": "Measure the image, or let the file convention do it",
767
+ "snippet": "// app/…/opengraph-image.tsx — `size` is the truth and the declaration at once\nexport const size = { width: 1200, height: 630 };\n\n// Naming a file instead? Then the numbers are that file's, not a default.\nopenGraph: { images: [{ url: cover.url, width: cover.width, height: cover.height }] }",
768
+ "language": "tsx"
769
+ }
770
+ },
698
771
  {
699
772
  "id": "og.locale.alternates",
700
773
  "scope": "page",
@@ -952,6 +1025,17 @@
952
1025
  "google-canonicalization"
953
1026
  ]
954
1027
  },
1028
+ {
1029
+ "id": "sitemap.entry.out-of-scope",
1030
+ "scope": "site",
1031
+ "severity": "error",
1032
+ "kind": "boolean",
1033
+ "summary": "A sitemap may only list URLs under its own directory",
1034
+ "rigor": "spec-required",
1035
+ "sources": [
1036
+ "sitemaps-protocol"
1037
+ ]
1038
+ },
955
1039
  {
956
1040
  "id": "sitemap.entry.protocol-mismatch",
957
1041
  "scope": "site",
@@ -1022,6 +1106,17 @@
1022
1106
  "google-sitemaps"
1023
1107
  ]
1024
1108
  },
1109
+ {
1110
+ "id": "sitemap.limits.exceeded",
1111
+ "scope": "site",
1112
+ "severity": "error",
1113
+ "kind": "boolean",
1114
+ "summary": "No single sitemap document may exceed 50,000 entries or 50 MB",
1115
+ "rigor": "spec-required",
1116
+ "sources": [
1117
+ "sitemaps-protocol"
1118
+ ]
1119
+ },
1025
1120
  {
1026
1121
  "id": "sitemap.missing",
1027
1122
  "scope": "site",