@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/README.md +345 -19
- package/dist/{chunk-3777NRM4.js → chunk-GFM3YN5N.js} +753 -198
- package/dist/chunk-GFM3YN5N.js.map +1 -0
- package/dist/cli.js +94 -12
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +548 -1
- package/dist/index.js +1 -1
- package/package.json +4 -3
- package/dist/chunk-3777NRM4.js.map +0 -1
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
|
|
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
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goflag/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
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": ">=
|
|
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"
|