@goflag/cli 0.1.3 → 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/dist/index.d.ts CHANGED
@@ -1,3 +1,52 @@
1
+ /**
2
+ * Canonical Goflag data model.
3
+ *
4
+ * Everything the engine, rules, suggestions, previews, snapshots, diff, and
5
+ * report layers consume comes from this single shape. The `Page` is the
6
+ * entire deterministic output of fetching + parsing one URL — no behavior,
7
+ * no side effects, fully serializable to JSON.
8
+ *
9
+ * Design constraints (see PLAN.md "Architectural non-negotiables"):
10
+ *
11
+ * - **No DOM, no React, no Next.js types here.** This module must be safely
12
+ * importable from a Node CLI, a browser bundle, an edge worker, or a
13
+ * future `@goflag/core` package consumed by a hosted SaaS.
14
+ * - **Stable contract.** Adding fields is fine; removing or renaming is a
15
+ * semver-major event. Snapshot files (Phase 9) and `--json` consumers
16
+ * (Phase 11.5) depend on this shape.
17
+ * - **Origins, not just values.** For most metadata we record both the
18
+ * parsed value AND where it came from (which raw tag, attribute, or
19
+ * JSON-LD path). This is what powers Phase 4's "what if I remove
20
+ * `og:image`?" toggle and the Phase 9 diff classifier.
21
+ */
22
+ /**
23
+ * Where a parsed value originated. Used by previews to highlight which tag a
24
+ * given preview consumed and by rules/suggestions to point the user back at
25
+ * the exact line in the source.
26
+ */
27
+ type TagOrigin = {
28
+ kind: "title";
29
+ } | {
30
+ kind: "meta";
31
+ name?: string;
32
+ property?: string;
33
+ httpEquiv?: string;
34
+ } | {
35
+ kind: "link";
36
+ rel: string;
37
+ } | {
38
+ kind: "html";
39
+ attribute: string;
40
+ } | {
41
+ kind: "json-ld";
42
+ path: string;
43
+ index: number;
44
+ } | {
45
+ kind: "header";
46
+ name: string;
47
+ } | {
48
+ kind: "computed";
49
+ };
1
50
  type Severity = "error" | "warning" | "info";
2
51
 
3
52
  /**
@@ -57,6 +106,424 @@ interface I18nMatrix {
57
106
  /** The outcome of probing a single URL. */
58
107
  type LinkVerdict = "ok" | "redirect" | "broken" | "blocked" | "warning" | "skipped";
59
108
 
109
+ /**
110
+ * The extraction model — goflag's per-page observation contract.
111
+ *
112
+ * This is the single normalized snapshot that deterministic rules and AI
113
+ * agents both read (rules-catalog plan §7). Rules never touch raw HTML;
114
+ * they read the documented fields below, which makes them testable in
115
+ * isolation and makes run-to-run diffing tractable.
116
+ *
117
+ * Design constraints:
118
+ *
119
+ * - **Versioned.** `extractionVersion` is bumped whenever a field is removed
120
+ * or reshaped. Adding optional fields is not a bump.
121
+ * - **Provenance-carrying.** Scalar observations are `Fact`s: value + which
122
+ * tag produced it + the literal string before normalization. An agent can
123
+ * always answer "says who?" from the extraction alone.
124
+ * - **Self-contained and serializable.** Plain JSON data, no methods, no
125
+ * references back into the engine. The shapes mirror `core/types.ts`
126
+ * (`Page`) deliberately — the adapter in `./from-page.ts` is mostly a
127
+ * projection — but they are declared here so the contract can hold still
128
+ * while the engine's internal `Page` evolves.
129
+ * - **Per-page.** Site-level artefacts (robots.txt, the sitemap tree) get
130
+ * their own observation models — `RobotsExtraction` / `SitemapExtraction`,
131
+ * specified in `docs/sitemap-robots-plan.md` — in Phase G. Site-computed
132
+ * i18n knowledge (locale cluster, reciprocity) and document structure
133
+ * (headings, landmarks) are reserved for future additive fields.
134
+ */
135
+
136
+ /**
137
+ * Bumped whenever the shape changes incompatibly (field removed or
138
+ * reshaped). Consumers that persist extractions (baselines, fingerprints)
139
+ * key on it.
140
+ */
141
+ declare const EXTRACTION_VERSION: 1;
142
+ /**
143
+ * One observed value and where it came from. `raw` is the literal string as
144
+ * it appeared in the source, kept whenever normalization (trimming, URL
145
+ * resolution, number parsing) changed it.
146
+ */
147
+ interface Fact<T> {
148
+ value: T;
149
+ origin: TagOrigin;
150
+ raw?: string;
151
+ }
152
+ /** The network-level observation: what the server said, before any parsing. */
153
+ interface ExtractionHttp {
154
+ /** The URL the audit asked for. */
155
+ requestedUrl: string;
156
+ /** The URL after following redirects (equals `requestedUrl` when none). */
157
+ finalUrl: string;
158
+ status: number;
159
+ /** Lowercased response headers from the final response. */
160
+ headers: Record<string, string>;
161
+ /** Number of redirects followed to reach `finalUrl`. */
162
+ redirects: number;
163
+ /** From `content-type`, lowercased, parameters stripped. */
164
+ contentType?: string;
165
+ }
166
+ /**
167
+ * How the observed DOM was produced. `static` is what a non-JS crawler sees;
168
+ * `headless` is the post-hydration DOM a JS-aware crawler sees. `escalated`
169
+ * means goflag started static, found the head suspiciously empty, and re-ran
170
+ * in headless mode — itself evidence that the page client-renders its
171
+ * metadata.
172
+ */
173
+ interface ExtractionRendering {
174
+ mode: "static" | "headless";
175
+ escalated: boolean;
176
+ escalationReason?: string;
177
+ }
178
+ /** Document-level observations: the root element and head-wide declarations. */
179
+ interface ExtractionDocument {
180
+ /** `<title>` text, trimmed. */
181
+ title?: Fact<string>;
182
+ /** `lang` attribute on `<html>`. */
183
+ lang?: Fact<string>;
184
+ /** `dir` attribute on `<html>`. */
185
+ dir?: Fact<string>;
186
+ /** Declared character encoding (`<meta charset>` or http-equiv). */
187
+ charset?: Fact<string>;
188
+ /** `<base href>`, when present. */
189
+ base?: Fact<string>;
190
+ }
191
+ /**
192
+ * Named metadata: `<meta name="…">` values plus the canonical link, which
193
+ * lives here (not under `links`) because rules consume it as a single
194
+ * page-level declaration. `canonical` is resolved to an absolute URL when
195
+ * possible; `raw` keeps what the tag literally said.
196
+ */
197
+ interface ExtractionMeta {
198
+ description?: Fact<string>;
199
+ viewport?: Fact<string>;
200
+ robots?: Fact<string>;
201
+ googlebot?: Fact<string>;
202
+ canonical?: Fact<string>;
203
+ themeColor?: Fact<string>;
204
+ colorScheme?: Fact<string>;
205
+ referrer?: Fact<string>;
206
+ generator?: Fact<string>;
207
+ applicationName?: Fact<string>;
208
+ author?: Fact<string>;
209
+ keywords?: Fact<string[]>;
210
+ }
211
+ /** One `og:image` structured object with its sub-properties. */
212
+ interface ExtractionOpenGraphImage {
213
+ url: Fact<string>;
214
+ secureUrl?: Fact<string>;
215
+ type?: Fact<string>;
216
+ width?: Fact<number>;
217
+ height?: Fact<number>;
218
+ alt?: Fact<string>;
219
+ }
220
+ /** The `og:*` surface (ogp.me vocabulary). */
221
+ interface ExtractionOpenGraph {
222
+ title?: Fact<string>;
223
+ type?: Fact<string>;
224
+ url?: Fact<string>;
225
+ description?: Fact<string>;
226
+ siteName?: Fact<string>;
227
+ locale?: Fact<string>;
228
+ localeAlternates: Fact<string>[];
229
+ images: ExtractionOpenGraphImage[];
230
+ /** Any `og:*` property not mapped above, kept verbatim. */
231
+ other: Array<{
232
+ property: string;
233
+ value: Fact<string>;
234
+ }>;
235
+ }
236
+ /** The `twitter:*` card surface. X falls back to `og:*` for absent fields. */
237
+ interface ExtractionTwitter {
238
+ card?: Fact<string>;
239
+ site?: Fact<string>;
240
+ creator?: Fact<string>;
241
+ title?: Fact<string>;
242
+ description?: Fact<string>;
243
+ image?: Fact<string>;
244
+ imageAlt?: Fact<string>;
245
+ }
246
+ /** One `<link rel="alternate" hreflang>` annotation, href resolved. */
247
+ interface ExtractionHreflang {
248
+ hreflang: string;
249
+ href: string;
250
+ isXDefault: boolean;
251
+ }
252
+ /** One icon link (`icon`, `apple-touch-icon`, …). */
253
+ interface ExtractionIcon {
254
+ rel: string;
255
+ href: string;
256
+ sizes?: string;
257
+ type?: string;
258
+ /** Parsed `sizes` entries; empty for unparseable values. */
259
+ parsedSizes: Array<{
260
+ width: number;
261
+ height: number;
262
+ } | "any">;
263
+ }
264
+ /** One feed advertisement (`rel="alternate"` with a feed content type). */
265
+ interface ExtractionFeed {
266
+ type: string;
267
+ href: string;
268
+ title?: string;
269
+ }
270
+ /**
271
+ * Head link observations other than canonical (which lives under `meta`).
272
+ * These are already-normalized collections rather than `Fact`s: their origin
273
+ * is unambiguous (the `link` tag their `rel` names).
274
+ */
275
+ interface ExtractionLinks {
276
+ hreflang: ExtractionHreflang[];
277
+ icons: ExtractionIcon[];
278
+ manifest?: {
279
+ href: string;
280
+ };
281
+ feeds: ExtractionFeed[];
282
+ }
283
+ /** One `<script type="application/ld+json">` block. */
284
+ interface ExtractionJsonLd {
285
+ /** Position in document order (0-based). */
286
+ index: number;
287
+ /** Top-level `@type` values found in the block (covers `@graph`). */
288
+ types: string[];
289
+ /** Parsed JSON, or `null` when parsing failed (see `parseError`). */
290
+ data: unknown;
291
+ parseError?: string;
292
+ /** Verbatim script content — the evidence, especially on parse errors. */
293
+ raw: string;
294
+ }
295
+ /**
296
+ * The per-page observation. Everything a page rule (or an agent judging a
297
+ * prose rule) may read; nothing else.
298
+ */
299
+ interface Extraction {
300
+ extractionVersion: typeof EXTRACTION_VERSION;
301
+ /** When the underlying fetch happened (ISO string, UTC). */
302
+ fetchedAt: string;
303
+ http: ExtractionHttp;
304
+ rendering: ExtractionRendering;
305
+ document: ExtractionDocument;
306
+ meta: ExtractionMeta;
307
+ openGraph: ExtractionOpenGraph;
308
+ twitter: ExtractionTwitter;
309
+ links: ExtractionLinks;
310
+ jsonLd: ExtractionJsonLd[];
311
+ }
312
+
313
+ /**
314
+ * The goflag rule descriptor contract (rules-catalog plan §6).
315
+ *
316
+ * A rule is a declarative descriptor — id, rigor, cited sources, which
317
+ * extraction paths it reads — plus a thin pure evaluator over the
318
+ * `Extraction` observation model. Rules never touch raw HTML and never see
319
+ * the engine's `Page`; the adapter in `../extraction/from-page.ts` is the
320
+ * only bridge. Every verdict is uniform and self-explaining: **observed vs
321
+ * expected vs source**.
322
+ *
323
+ * Two axes matter and are deliberately separate:
324
+ *
325
+ * - **kind** (how we evaluate): `boolean` rules pass or fail mechanically;
326
+ * `scored` rules place a measurement in a band (`ideal` / `acceptable` /
327
+ * `poor`). Both may answer `na` when the rule does not apply to this page
328
+ * — absence of an optional subject is not a failure. `prose` rules
329
+ * (`ProseRule`, below) evaluate to nothing at all: they carry evidence to
330
+ * an agent instead of guessing, and are kept out of the deterministic
331
+ * `Rule` union so no consumer can mistake one for a verdict.
332
+ * - **rigor** (how authoritative the requirement is): `spec-required` ›
333
+ * `spec-recommended` › `vendor-spec` › `guideline` › `heuristic`. This is
334
+ * the honest expression of "source of truth" — an agent must never fix a
335
+ * `heuristic` as if it were `spec-required`. Every rule cites ≥1 `Source`
336
+ * from `./sources` (CI-enforced in `rules.test.ts`).
337
+ *
338
+ * The `Issue` shape still lives in `../core/types` because it crosses the
339
+ * engine → report → CLI `--json` boundary; `./evaluate.ts` converts
340
+ * findings into issues at that boundary.
341
+ */
342
+
343
+ /** How authoritative the requirement behind a rule is. */
344
+ type Rigor = "spec-required" | "spec-recommended" | "vendor-spec" | "guideline" | "heuristic";
345
+ /** A band a scored measurement can land in. */
346
+ type Band = "ideal" | "acceptable" | "poor";
347
+ /** Everything a rule declares about itself, before any evaluation. */
348
+ interface RuleBase {
349
+ /** Stable identifier in `category.short-name` form (e.g. `title.missing`). */
350
+ id: string;
351
+ /** Grouping key: `document`, `meta`, `opengraph`, `robots`, … */
352
+ category: string;
353
+ /** One-line statement of the policy (shown in docs and `why`). */
354
+ title: string;
355
+ /** Rationale: why honoring this rule matters. */
356
+ why: string;
357
+ rigor: Rigor;
358
+ /** Ids into the source catalog (`./sources`). ≥1, CI-enforced. */
359
+ sources: string[];
360
+ /** Dotted paths into `Extraction` this rule reads (e.g. `meta.canonical`). */
361
+ reads: string[];
362
+ /** Related rule ids, when useful. */
363
+ relates?: string[];
364
+ /**
365
+ * Copy-pasteable remediation snippet, attached to failing findings.
366
+ * Deliberately minimal — real fix generation is the M2 work.
367
+ */
368
+ fix?: {
369
+ title: string;
370
+ snippet: string;
371
+ language: string;
372
+ };
373
+ }
374
+ /** What a boolean rule's evaluator returns. */
375
+ interface BooleanVerdict {
376
+ /** `na` means the rule does not apply to this page (not a pass). */
377
+ status: "pass" | "fail" | "na";
378
+ /** The relevant observed value, JSON-serializable. */
379
+ observed: unknown;
380
+ /** Human sentence describing failure (only read when `status === "fail"`). */
381
+ message?: string;
382
+ /** Pointer back into the page for highlighting. */
383
+ origin?: TagOrigin;
384
+ }
385
+ /** A rule that passes or fails mechanically. */
386
+ interface BooleanRule extends RuleBase {
387
+ kind: "boolean";
388
+ /** Severity of a failing finding. */
389
+ severity: Severity;
390
+ /** One sentence stating what a passing page looks like. */
391
+ expected: string;
392
+ evaluate: (extraction: Extraction) => BooleanVerdict;
393
+ }
394
+ /** What a scored rule's evaluator returns. */
395
+ interface ScoredVerdict {
396
+ /** `na` when the measured subject is absent; `warn` outside the ideal band. */
397
+ status: "pass" | "warn" | "na";
398
+ band?: Band;
399
+ observed: number | string;
400
+ message?: string;
401
+ origin?: TagOrigin;
402
+ }
403
+ /** A rule that places a measurement in a band instead of pass/fail. */
404
+ interface ScoredRule extends RuleBase {
405
+ kind: "scored";
406
+ /** Inclusive `[min, max]` windows; `acceptable` should contain `ideal`. */
407
+ bands: {
408
+ ideal: [number, number];
409
+ acceptable: [number, number];
410
+ };
411
+ /** Severity of a finding landing in each non-ideal band. */
412
+ severityByBand: Record<"acceptable" | "poor", Severity>;
413
+ /** One sentence stating the ideal window. */
414
+ expected: string;
415
+ evaluate: (extraction: Extraction) => ScoredVerdict;
416
+ }
417
+ /**
418
+ * One prose rule's evidence bundle for one page. Deliberately *not* a
419
+ * verdict: `needs-judgment` is the only value `verdict` ever takes here.
420
+ * Nothing downstream may read an advisory as a finding — it never affects
421
+ * severity counts, the verdict, or the exit code, because no one has judged
422
+ * it yet.
423
+ */
424
+ interface AdvisoryFinding {
425
+ ruleId: string;
426
+ kind: "prose";
427
+ prose: string;
428
+ rigor: Rigor;
429
+ sources: string[];
430
+ /**
431
+ * Extraction path → the observed value there, one entry per declared
432
+ * path. An absent observation is present as `null` rather than missing:
433
+ * "the page has no `og:image`" is evidence, and a missing key could not be
434
+ * told apart from a path that resolved to nothing.
435
+ */
436
+ evidence: Record<string, unknown>;
437
+ verdict: "needs-judgment";
438
+ }
439
+ type Rule = BooleanRule | ScoredRule;
440
+ /**
441
+ * One rule's verdict on one page — the uniform result every consumer
442
+ * (report, conformance view, agents) reads. Produced by `./evaluate.ts`
443
+ * for **every** rule, passing ones included, so the conformance matrix
444
+ * (plan §10) is just this list unfiltered.
445
+ */
446
+ interface RuleFinding {
447
+ ruleId: string;
448
+ kind: Rule["kind"];
449
+ status: "pass" | "fail" | "warn" | "na";
450
+ band?: Band;
451
+ /** Set when `status` is `fail` or `warn`. */
452
+ severity?: Severity;
453
+ observed: unknown;
454
+ expected: string;
455
+ /** Human sentence; present on failing/warning findings. */
456
+ message?: string;
457
+ origin?: TagOrigin;
458
+ rigor: Rigor;
459
+ sources: string[];
460
+ }
461
+
462
+ /**
463
+ * The conformance view (plan §10) — every rule's status on every page, not
464
+ * just the violations.
465
+ *
466
+ * `seoIssues` answers "what is wrong here". That is the right default for a
467
+ * human fixing a site, and the wrong shape for the question the plan calls
468
+ * point #2: *where do we stand against the catalog?* A list of violations
469
+ * cannot distinguish "this rule passes everywhere" from "this rule never
470
+ * applied to a single page" — and an agent deciding what to work on needs
471
+ * exactly that distinction.
472
+ *
473
+ * So this is the same evaluation, unfiltered: a rule × page matrix of
474
+ * `pass` / `fail` / `warn` / `na`, plus per-rule totals. Opt-in, because on
475
+ * a 200-page crawl it is the largest thing in the report and nobody reading
476
+ * a terminal asked for it.
477
+ *
478
+ * Shape note: rule metadata (rigor, sources, expected) is carried once in
479
+ * `rules`, not repeated per cell. A 200-page × 11-rule matrix with inline
480
+ * metadata is ~2,200 copies of the same eight fields; the legend + status
481
+ * grid says the same thing at a fraction of the size, which matters when
482
+ * the consumer is a context window.
483
+ */
484
+
485
+ /** A rule's verdict on one page. Mirrors `RuleFinding["status"]`. */
486
+ type ConformanceStatus = RuleFinding["status"];
487
+ /** Per-rule legend entry and tally across every scanned page. */
488
+ interface ConformanceRule {
489
+ ruleId: string;
490
+ kind: Rule["kind"];
491
+ title: string;
492
+ rigor: Rigor;
493
+ sources: string[];
494
+ /** One sentence stating what a passing page looks like. */
495
+ expected: string;
496
+ /**
497
+ * How the rule landed across the run. `crashed` counts pages where the
498
+ * evaluator threw, so the five buckets always sum to the number of pages
499
+ * — a matrix whose arithmetic does not close is worse than no matrix.
500
+ */
501
+ totals: {
502
+ pass: number;
503
+ fail: number;
504
+ warn: number;
505
+ na: number;
506
+ crashed: number;
507
+ };
508
+ }
509
+ /** One page's row of the matrix. Rules that crashed on it are absent. */
510
+ interface ConformancePage {
511
+ url: string;
512
+ /** Rule id → status. Keyed, not positional, so it survives rule reordering. */
513
+ statuses: Record<string, ConformanceStatus>;
514
+ }
515
+ interface ConformanceView {
516
+ /** Legend + totals, in registry order. */
517
+ rules: ConformanceRule[];
518
+ /**
519
+ * One row per **linted** page, in crawl order — which is a smaller set than
520
+ * `report.pages`: unreachable pages, non-HTML resources, and pages the site
521
+ * itself declared duplicate via canonical never reach the rule layer. Read
522
+ * `pages.length` for the denominator; every rule's totals sum to it.
523
+ */
524
+ pages: ConformancePage[];
525
+ }
526
+
60
527
  /**
61
528
  * Baseline comparison.
62
529
  *
@@ -93,10 +560,23 @@ interface DiffEntry {
93
560
  pageUrl?: string;
94
561
  }
95
562
  interface ReportDiff {
96
- /** Where and when the baseline was taken, echoed for the report header. */
563
+ /** Where, when, and under which rule profile the baseline was taken. */
97
564
  baseline: {
98
565
  url: string;
99
566
  finishedAt: string;
567
+ profile: string;
568
+ };
569
+ /**
570
+ * Set when this run was judged under a different profile than the baseline
571
+ * was captured under. Not an error — comparing `spec-only` against a
572
+ * `default` baseline is legitimate, it is just not like-for-like, and
573
+ * "0 new findings" means less than it appears. So it is reported loudly
574
+ * and gates nothing: silently diffing across two policies is the failure
575
+ * mode worth preventing.
576
+ */
577
+ profileMismatch?: {
578
+ baseline: string;
579
+ current: string;
100
580
  };
101
581
  /** Findings present now and absent from the baseline — what should fail CI. */
102
582
  added: DiffEntry[];
@@ -211,11 +691,28 @@ interface SeoIssue {
211
691
  /** A copy-pasteable fix snippet, when the rule offers one. */
212
692
  fix?: string;
213
693
  }
694
+ /**
695
+ * One prose rule's evidence bundle, attributed to a page. The `id` is a
696
+ * fingerprint on the same scheme as `SeoIssue`, so an agent's judgment can
697
+ * be recorded against it and diffed across runs (M1) once judgments exist.
698
+ */
699
+ interface ReportAdvisory extends AdvisoryFinding {
700
+ id: string;
701
+ pageUrl: string;
702
+ }
214
703
  interface GoflagReport {
215
704
  /** The base URL the audit started from (normalized). */
216
705
  url: string;
217
706
  /** ISO timestamp when the audit finished. */
218
707
  finishedAt: string;
708
+ /**
709
+ * The rule profile the run was judged under (`--profile`, default
710
+ * `default`). Recorded because it changes what "clean" means: a baseline
711
+ * captured under `spec-only` is not comparable to one captured under
712
+ * `strict`, and a green run says nothing about the rules the profile
713
+ * switched off.
714
+ */
715
+ profile: string;
219
716
  summary: {
220
717
  brokenLinks: number;
221
718
  missingTranslations: number;
@@ -240,6 +737,20 @@ interface GoflagReport {
240
737
  seoIssues: SeoIssue[];
241
738
  /** Findings from the cross-page rule registry (`SITE_RULES`). */
242
739
  siteIssues: SiteIssue[];
740
+ /**
741
+ * Every rule's status on every page, passing ones included. Present only
742
+ * when asked for (`--conformance`): on a large crawl it dwarfs the rest
743
+ * of the report, and it answers a question — "where do we stand against
744
+ * the catalog?" — that a violations list cannot.
745
+ */
746
+ conformance?: ConformanceView;
747
+ /**
748
+ * Prose rules with their evidence, for an agent to judge. Present only
749
+ * when asked for (`--advisories`). Every entry's verdict is
750
+ * `needs-judgment`: these are questions, not findings, and they never
751
+ * touch the summary counts, the verdict, or the exit code.
752
+ */
753
+ advisories?: ReportAdvisory[];
243
754
  /** Comparison against a stored baseline, when `--baseline` was given. */
244
755
  diff?: ReportDiff;
245
756
  diagnostics: {
@@ -336,6 +847,24 @@ interface AuditOptions {
336
847
  * what made hreflang checks silently vacuous.
337
848
  */
338
849
  noSitemap?: boolean;
850
+ /**
851
+ * Named policy overlay applied to the rule registry (`--profile`), e.g.
852
+ * `strict` or `spec-only`. Defaults to `default` (no overlay). Throws on
853
+ * an unknown name rather than auditing under a policy nobody asked for.
854
+ */
855
+ profile?: string;
856
+ /**
857
+ * Emit the rule × page conformance matrix (`--conformance`): every rule's
858
+ * status on every page, passing ones included. Off by default because it
859
+ * is the largest section of the report and only an agent (or a coverage
860
+ * question) wants it.
861
+ */
862
+ conformance?: boolean;
863
+ /**
864
+ * Emit prose rules with their evidence bundles (`--advisories`). These
865
+ * carry no verdict and never affect the gate.
866
+ */
867
+ advisories?: boolean;
339
868
  }
340
869
  /**
341
870
  * Routes present in some locales but absent in others. A "hole" is exactly
@@ -444,6 +973,8 @@ interface RollupReciprocity {
444
973
  interface GoflagSummary {
445
974
  url: string;
446
975
  finishedAt: string;
976
+ /** The rule profile the run was judged under. See `GoflagReport.profile`. */
977
+ profile: string;
447
978
  verdict: Verdict;
448
979
  totals: {
449
980
  brokenLinks: number;
@@ -464,6 +995,22 @@ interface GoflagSummary {
464
995
  seoIssues: RollupSeo[];
465
996
  /** Cross-page findings, rolled up by rule exactly like `seoIssues`. */
466
997
  siteIssues: RollupSeo[];
998
+ /**
999
+ * Per-rule conformance totals, when `--conformance` was asked for. The
1000
+ * per-page matrix is dropped here on purpose — it is precisely the
1001
+ * page-by-page repetition summary mode exists to collapse, and the totals
1002
+ * answer the same question. Use the full report for the grid.
1003
+ */
1004
+ conformance?: {
1005
+ rules: ConformanceRule[];
1006
+ };
1007
+ /**
1008
+ * Prose rules needing judgment, when `--advisories` was asked for, carried
1009
+ * verbatim. Not rolled up: each page's evidence bundle is different, and
1010
+ * collapsing them would throw away the only thing that makes an advisory
1011
+ * answerable.
1012
+ */
1013
+ advisories?: ReportAdvisory[];
467
1014
  truncated: boolean;
468
1015
  warnings: string[];
469
1016
  }
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  runAudit,
12
12
  summarize,
13
13
  targetKey
14
- } from "./chunk-3777NRM4.js";
14
+ } from "./chunk-GFM3YN5N.js";
15
15
  export {
16
16
  Logger,
17
17
  SAMPLE_LIMIT,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@goflag/cli",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
- "description": "A lean CLI that audits a site for broken links, missing translation pages, and missing/misconfigured SEO metadata. JSON-first.",
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": [
7
7
  "seo",
8
8
  "hreflang",
@@ -28,7 +28,7 @@
28
28
  "url": "https://github.com/tancredesimonin/goflag/issues"
29
29
  },
30
30
  "engines": {
31
- "node": ">=20.11"
31
+ "node": ">=22"
32
32
  },
33
33
  "bin": {
34
34
  "goflag": "dist/cli.js"
@@ -58,6 +58,7 @@
58
58
  "test:unit": "vitest run src test/unit",
59
59
  "test:integration": "vitest run test/integration",
60
60
  "test:coverage": "vitest run --coverage",
61
+ "validate:sources": "tsx scripts/validate-sources.ts",
61
62
  "prepack": "node ../../scripts/stage-package-docs.mjs",
62
63
  "prepublishOnly": "pnpm build",
63
64
  "release": "commit-and-tag-version"