@event4u/agent-config 14.15.0 → 14.17.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.
Files changed (58) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +84 -153
  5. package/README.md +1 -1
  6. package/dist/agent-src/commands/analyze/inbox.md +172 -6
  7. package/dist/agent-src/commands/fix/commit-messages.md +336 -0
  8. package/dist/agent-src/commands/fix.md +5 -3
  9. package/dist/agent-src/rules/commit-conventions.md +1 -1
  10. package/dist/agent-src/rules/security-sensitive-stop.md +7 -5
  11. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +20 -2
  12. package/dist/agent-src/skills/aws-infrastructure/SKILL.md +41 -0
  13. package/dist/agent-src/skills/conventional-commits-writing/SKILL.md +234 -4
  14. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +99 -99
  15. package/dist/agent-src/skills/fe-design/SKILL.md +6 -3
  16. package/dist/agent-src/skills/fe-design/references/design-patterns.md +28 -0
  17. package/dist/agent-src/skills/iconography/SKILL.md +37 -6
  18. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  19. package/dist/agent-src/skills/terraform/SKILL.md +29 -0
  20. package/dist/agent-src/skills/threat-modeling/data/reasoning.csv +1 -0
  21. package/dist/agent-src/skills/threat-modeling/data/surfaces.csv +1 -0
  22. package/dist/agent-src/skills/threat-modeling/data/threats.csv +5 -0
  23. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  24. package/dist/cli-delegate/cmd_conformance.js +158 -33
  25. package/dist/cli-delegate/cmd_explain.js +24 -112
  26. package/dist/discovery/deprecation-report.md +1 -1
  27. package/dist/discovery/discovery-manifest.json +47 -21
  28. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  29. package/dist/discovery/discovery-manifest.summary.md +3 -3
  30. package/dist/discovery/dormancy-report.md +1 -1
  31. package/dist/discovery/orphan-report.md +1 -1
  32. package/dist/discovery/packs.json +6 -5
  33. package/dist/discovery/trust-report.md +2 -2
  34. package/dist/discovery/workspaces.json +5 -4
  35. package/dist/hooks/dispatch.js +169 -41
  36. package/dist/mcp/registry-manifest.json +3 -3
  37. package/dist/mcp/server.json +2 -2
  38. package/dist/mcp/server.mjs +160 -35
  39. package/dist/router.json +1 -1
  40. package/docs/guidelines/design-antipatterns.md +10 -2
  41. package/package.json +1 -1
  42. package/src/config/gate-violation-baselines.json +2 -2
  43. package/src/config/web-launch-readiness.json +1 -1
  44. package/src/scripts/_cli/explain_last/index.ts +1 -1
  45. package/src/scripts/_cli/explain_last/memory.ts +8 -104
  46. package/src/scripts/check_artefact_count_messaging.ts +9 -0
  47. package/src/scripts/check_release_highlights.ts +77 -4
  48. package/src/scripts/check_web_launch_readiness.ts +295 -15
  49. package/src/scripts/design_slop_rules.ts +19 -9
  50. package/src/scripts/git_authorization_hook.ts +257 -35
  51. package/src/scripts/hooks/block_unauthorized_git.ts +16 -4
  52. package/src/scripts/hooks/merge_impact.ts +68 -0
  53. package/src/scripts/measure_release_mix.ts +384 -0
  54. package/src/scripts/release.ts +4 -17
  55. package/src/scripts/release_drill.ts +154 -1
  56. package/src/scripts/release_mix_taxonomy.json +172 -0
  57. package/src/scripts/release_publication.ts +91 -0
  58. package/src/scripts/attest_artifact.ts +0 -193
@@ -2,12 +2,12 @@
2
2
  * Highlight plausibility gate (release-truth Phase 2).
3
3
  *
4
4
  * The recorded failure: the curated release head said `_none_` twice while
5
- * false — 9.13.0 shipped behaviour changes, a removed public trigger type, a
5
+ * false — 9.13.0 shipped behavior changes, a removed public trigger type, a
6
6
  * security fix, and honest nulls; the head claimed none of it. Same at
7
7
  * 9.14.0.
8
8
  *
9
9
  * This gate derives GENERATED evidence categories from the release span
10
- * (security-tagged commits, behaviour/default changes from conventional
10
+ * (security-tagged commits, behavior/default changes from conventional
11
11
  * commit types + rule/schema diffs, honest-null markers, removed public
12
12
  * surface) and FAILS when a populated generated category meets a `_none_`
13
13
  * curated field. It blocks the contradiction only — a human still writes the
@@ -23,7 +23,7 @@
23
23
  * ## REVERSED 2026-09-01 — an unrewritten derived line now BLOCKS
24
24
  *
25
25
  * This header used to end the paragraph above with *"An unrewritten derived
26
- * line warns; it never blocks."* That is no longer the behaviour, and the
26
+ * line warns; it never blocks."* That is no longer the behavior, and the
27
27
  * sentence is replaced rather than deleted so the reversal is legible.
28
28
  *
29
29
  * **The premise was never wrong; the conclusion drawn from it was.** A derived
@@ -74,6 +74,7 @@ import {
74
74
  previous_release_tag,
75
75
  stale_draft_labels,
76
76
  } from './_lib/release_highlights.js';
77
+ import { loadTaxonomy, measureRange } from './measure_release_mix.js';
77
78
  import {
78
79
  CURATED_HEAD_INSTRUCTION,
79
80
  extract_changelog_section,
@@ -149,7 +150,7 @@ export function highlight_contradictions(
149
150
  * Exported so the advisory-vs-blocking decision is a fixture, not a claim.
150
151
  *
151
152
  * CORRECTED 2026-09-01 with the reversal. This comment used to say that
152
- * `stale_draft_labels` firing while the exit code stays 0 is the behaviour the
153
+ * `stale_draft_labels` firing while the exit code stays 0 is the behavior the
153
154
  * cadence blocker adjudicates. That is no longer what happens — the branch at
154
155
  * the call site returns 1 — and the sentence is replaced rather than deleted
155
156
  * because it was the load-bearing claim on the other side.
@@ -313,6 +314,10 @@ export function main(argv: readonly string[]): number {
313
314
  }
314
315
  const contradictions = highlight_contradictions(curated, derived);
315
316
  if (contradictions.length === 0) {
317
+ const mix = check_governance_mix_response(section.body, from, to, version);
318
+ if (mix !== 0) {
319
+ return mix;
320
+ }
316
321
  process.stdout.write(`✅ curated head plausible for ${version} (span ${from}..${to})\n`);
317
322
  return 0;
318
323
  }
@@ -332,6 +337,74 @@ export function main(argv: readonly string[]): number {
332
337
  return 1;
333
338
  }
334
339
 
340
+ /**
341
+ * The governance-versus-product response, checked for PRESENCE, never for the
342
+ * ratio (ADR-253).
343
+ *
344
+ * `measure_release_mix` publishes a level. This is the one place that level has
345
+ * a consequence, and the consequence is deliberately about completeness of the
346
+ * written answer: when governance-only commits strictly outnumber consumer-only
347
+ * commits over the release span, the section under release must carry a written
348
+ * response naming either the next cycle's consumer work or a maintainer
349
+ * justification. No number is enforced, and no number can be — both council
350
+ * seats refused a threshold on fewer than two readings, and a gate with no
351
+ * threshold that blocks on the ratio is a smuggled threshold.
352
+ *
353
+ * The response lives OUTSIDE the curated head, as its own `> **Governance
354
+ * mix:**` line, so it does not consume the head's ten-line cap and does not
355
+ * become a sixth label every historical section would suddenly lack.
356
+ *
357
+ * Scope matches every other check here: only the section under release is read.
358
+ *
359
+ * A measurement failure DEGRADES to a printed warning rather than to a refusal.
360
+ * This is a governance signal, not a correctness or security control, and a
361
+ * shallow clone or a missing tag on the release path would otherwise turn an
362
+ * unrelated environment fact into a blocked release.
363
+ */
364
+ export const MIX_RESPONSE_MARKER = '**Governance mix:**';
365
+ const MIX_RESPONSE_MIN_CHARS = 40;
366
+
367
+ export function check_governance_mix_response(
368
+ body: string,
369
+ from: string,
370
+ to: string,
371
+ version: string,
372
+ ): number {
373
+ let reading;
374
+ try {
375
+ reading = measureRange(from, to, loadTaxonomy(), version, REPO_ROOT);
376
+ } catch (err) {
377
+ process.stdout.write(
378
+ `⚠️ governance mix not measured for ${version}: ${(err as Error).message}\n`,
379
+ );
380
+ return 0;
381
+ }
382
+ const o = reading.response_obligation;
383
+ const level = `governance-only ${o.governance_only} vs consumer-only ${o.consumer_only} (taxonomy ${reading.taxonomy_version})`;
384
+ if (!o.triggered) {
385
+ process.stdout.write(`✅ governance mix for ${version}: ${level} — no response owed\n`);
386
+ return 0;
387
+ }
388
+ const idx = body.indexOf(MIX_RESPONSE_MARKER);
389
+ const written = idx === -1 ? '' : body.slice(idx + MIX_RESPONSE_MARKER.length).split('\n')[0]!.trim();
390
+ if (written.length >= MIX_RESPONSE_MIN_CHARS) {
391
+ process.stdout.write(`✅ governance mix for ${version}: ${level} — response present\n`);
392
+ return 0;
393
+ }
394
+ process.stderr.write(
395
+ `❌ the ${version} section owes a governance-versus-product response: ${level}.\n` +
396
+ ' Governance-only commits outnumber consumer-only commits over the release span, so\n' +
397
+ ' the section carries a written response naming either the next cycle\'s consumer work\n' +
398
+ ' or a maintainer justification (docs/contracts/CHANGELOG-conventions.md § Governance-\n' +
399
+ ' versus-product response; the decline this replaces is ADR-253).\n' +
400
+ ` Add one line under the ${version} curated head, for example:\n` +
401
+ ` > ${MIX_RESPONSE_MARKER} ${level}. Next cycle ships <the consumer work>.\n` +
402
+ ' Only that section is read; historical sections are out of scope. No ratio is\n' +
403
+ ' enforced — this blocks a MISSING answer, never a particular number.\n',
404
+ );
405
+ return 1;
406
+ }
407
+
335
408
  const _isMain = (() => {
336
409
  const entry = process.argv[1];
337
410
  if (!entry) return false;
@@ -100,8 +100,23 @@ export interface Undecided {
100
100
  /** `file:line` the instrument stopped at, so the reader can look at it. */
101
101
  location: string;
102
102
  reason: string;
103
+ /** WHY it could not decide — see `UndecidedReason`. Never a fifth state. */
104
+ reason_class: UndecidedReason;
103
105
  }
104
106
 
107
+ /**
108
+ * The two ways a check can fail to decide. A REASON CLASS on the existing
109
+ * `Undecided` state, deliberately not a new state.
110
+ *
111
+ * `instrument-limit` the input carries no evidence this reader can consume.
112
+ * A static reader looking for `<img>` in a tree with no
113
+ * HTML page is not finding zero violations — it is
114
+ * reading zero bytes, and those are different answers.
115
+ * `declaration-missing` the evidence is readable, but the CALLER declared
116
+ * nothing that would settle which rule applies to it.
117
+ */
118
+ export type UndecidedReason = 'instrument-limit' | 'declaration-missing';
119
+
105
120
  /** A tier the region axis moved, and why. Reported so the axis is visible. */
106
121
  export interface Escalated {
107
122
  check: string;
@@ -337,14 +352,35 @@ export function tierFor(
337
352
  return e === undefined ? { tier: c.tier, escalatedBy: null } : { tier: e.to, escalatedBy: e.why };
338
353
  }
339
354
 
340
- /** `web_launch_readiness.enabled: true` in `.agent-settings.yml` — default OFF. */
341
- export function enabled(root: string): boolean {
355
+ /**
356
+ * The `web_launch_readiness:` block of `.agent-settings.yml`, as read.
357
+ *
358
+ * `site_type` and `region` join `enabled` here because they were CLI-ONLY, and
359
+ * that is D12: a consumer could set `enabled: true`, run the gate, and get
360
+ * `required-legal-pages` at SITUATIONAL — non-blocking, exit 0 — while the same
361
+ * finding is CRITICAL and blocking for a DE-targeted site. The escalation was
362
+ * not overridden; it was never reachable, because `--region` defaults to
363
+ * `unspecified` and no settings key could say otherwise.
364
+ *
365
+ * `undefined` means the key is ABSENT, which is a different fact from
366
+ * `region: 'unspecified'` — the first is a caller who said nothing, the second
367
+ * a caller who said "no jurisdiction". `render()` distinguishes them.
368
+ */
369
+ export interface WebLaunchSettings {
370
+ enabled: boolean;
371
+ site_type?: SiteType;
372
+ region?: Region;
373
+ }
374
+
375
+ /** Two-space keys under `web_launch_readiness:` in `.agent-settings.yml`. */
376
+ export function readSettings(root: string): WebLaunchSettings {
342
377
  let raw: string;
343
378
  try {
344
379
  raw = fs.readFileSync(path.join(root, '.agent-settings.yml'), 'utf-8');
345
380
  } catch {
346
- return false;
381
+ return { enabled: false };
347
382
  }
383
+ const out: WebLaunchSettings = { enabled: false };
348
384
  let inSection = false;
349
385
  for (const line of raw.split('\n')) {
350
386
  if (/^web_launch_readiness:\s*$/.test(line)) {
@@ -355,12 +391,22 @@ export function enabled(root: string): boolean {
355
391
  inSection = false;
356
392
  continue;
357
393
  }
358
- if (inSection && /^\s{2}enabled:\s*true\s*$/.test(line)) return true;
394
+ if (!inSection) continue;
395
+ if (/^\s{2}enabled:\s*true\s*$/.test(line)) out.enabled = true;
396
+ const st = /^\s{2}site_type:\s*["']?([a-z-]+)["']?\s*$/.exec(line);
397
+ if (st !== null) out.site_type = st[1] as SiteType;
398
+ const rg = /^\s{2}region:\s*["']?([a-z]+)["']?\s*$/.exec(line);
399
+ if (rg !== null) out.region = rg[1] as Region;
359
400
  }
360
- return false;
401
+ return out;
402
+ }
403
+
404
+ /** `web_launch_readiness.enabled: true` in `.agent-settings.yml` — default OFF. */
405
+ export function enabled(root: string): boolean {
406
+ return readSettings(root).enabled;
361
407
  }
362
408
 
363
- /* ── the one implemented check: staging indexability ───────────────────────── */
409
+ /* -- staging indexability: the check that predates the IMPLS table --------- */
364
410
 
365
411
  const NOINDEX_RE = /<meta[^>]+name\s*=\s*["']robots["'][^>]*content\s*=\s*["'][^"']*noindex/i;
366
412
  const XROBOTS_RE = /x-robots-tag[^\n]*noindex/i;
@@ -778,11 +824,153 @@ const analyticsAndConsent: Impl = (files) => analyseConsentOrder(files).hits;
778
824
  * EMPTY `Located[]` means "applied and found nothing" is what keeps the
779
825
  * silent-green defect out of `IMPLS`, and folding a third state into that return
780
826
  * type would put the invariant back in play for all eight implementations.
827
+ *
828
+ * EVERY check has an entry, and the entries are NOT interchangeable. Measured
829
+ * on this tree before the table was filled: a build holding only `index.js` and
830
+ * `src/App.vue` — one `<img>` with no alt, no `<title>`, no `lang`, no charset,
831
+ * no viewport, no canonical — produced
832
+ *
833
+ * PASSED: staging-noindex-leftover, https-enforcement, per-route-metadata,
834
+ * image-alternative-text, document-head-basics,
835
+ * canonical-and-sitemap-coherence, analytics-and-consent-wiring
836
+ * exit 0
837
+ *
838
+ * Five of those iterated ZERO files, because `isPage()` filtered every file out.
839
+ * The `Impl` contract above states the rule they broke in its own words, and
840
+ * `audit()` turned each empty array into a pass.
841
+ *
842
+ * The probes are keyed to EVIDENCE SURFACE, not to one shared blindness
843
+ * predicate. An AI council (2026-09-04, 2/2, anthropic + openai) rejected the
844
+ * single predicate: "no HTML page" is honest for a check that reads rendered
845
+ * markup and semantically wrong for one that matches route paths, where the
846
+ * absence of HTML does not establish the absence of a route surface. Four
847
+ * surfaces, nine checks.
848
+ */
849
+ interface UndecideHit extends Located {
850
+ reason_class: UndecidedReason;
851
+ /**
852
+ * The instrument could not read this check's evidence surface AT ALL, so
853
+ * whatever the `Impl` returned came from the same zero evidence and must
854
+ * not be reported.
855
+ *
856
+ * Without this, `custom-error-route` on a component-rendered build reports
857
+ * "no 404.html in the build output" as a HIGH finding AND, two lines lower,
858
+ * that the absence of the file establishes nothing. Both sentences are
859
+ * printed from the same read, and one of them is wrong. A PER-LOCATION
860
+ * undecided (the consent-order pass) is the opposite case: other locations
861
+ * were decided, so its findings stand and the cover legitimately overlaps.
862
+ */
863
+ surface_blind?: true;
864
+ }
865
+ export interface UndecideCtx {
866
+ region: Region;
867
+ siteType: SiteType;
868
+ escalations: readonly Escalation[];
869
+ }
870
+ export type Undecide = (files: readonly SourceFile[], ctx: UndecideCtx) => UndecideHit[];
871
+
872
+ /** Surface 1 — anything at all was read. */
873
+ const readAnything = (files: readonly SourceFile[]): boolean => files.length > 0;
874
+
875
+ /** Surface 2 — a rendered HTML page exists, so markup is inspectable. */
876
+ const hasPage = (files: readonly SourceFile[]): boolean => files.some(isPage);
877
+
878
+ /** Surface 3 — code the consent pass can read at all (`.md` is prose, not code). */
879
+ const hasCode = (files: readonly SourceFile[]): boolean => files.some((f) => !/\.md$/i.test(f.base));
880
+
881
+ const ROUTE_DIR_RE = /(^|\/)(routes?|pages)(\/|$)/i;
882
+ const ROUTER_FILE_RE = /^(router|routes)\.(js|jsx|ts|tsx|mjs|cjs)$/i;
883
+
884
+ /**
885
+ * Surface 4 — a statically inspectable ROUTE surface.
886
+ *
887
+ * Enumerated rather than described, because a predicate is only as good as the
888
+ * artefacts it names: an HTML page, a `sitemap.xml`, a `routes/` or `pages/`
889
+ * tree, or a router module. None of the four present means a 404 route or a
890
+ * legal page can exist entirely inside a client bundle, and its absence AS A
891
+ * FILE establishes nothing. One of the four present and the absence IS the
892
+ * finding — which is why this is not the markup predicate wearing another name.
781
893
  */
782
- type Undecide = (files: readonly SourceFile[]) => Located[];
894
+ const hasRouteSurface = (files: readonly SourceFile[]): boolean =>
895
+ files.some(
896
+ (f) =>
897
+ isPage(f) ||
898
+ f.base === 'sitemap.xml' ||
899
+ ROUTE_DIR_RE.test(f.rel.replace(/\\/g, '/')) ||
900
+ ROUTER_FILE_RE.test(f.base),
901
+ );
902
+
903
+ /** A whole-tree undecided: an absence has no line, and inventing one would lie. */
904
+ const blind = (text: string, reason_class: UndecidedReason = 'instrument-limit'): UndecideHit[] => [
905
+ { file: '.', line: 0, text, reason_class, surface_blind: true },
906
+ ];
907
+
908
+ /** `p(files)` true → decidable, no entry. False → one whole-tree undecided. */
909
+ const unless = (p: (f: readonly SourceFile[]) => boolean, text: string): Undecide =>
910
+ (files) => (p(files) ? [] : blind(text));
911
+
912
+ const NO_MARKUP =
913
+ 'no HTML page in this build tree, so this check read zero bytes rather than finding zero ' +
914
+ 'violations. Markup is produced by components or a template engine that a static reader ' +
915
+ 'does not evaluate';
783
916
 
784
917
  export const UNDECIDABLE: Readonly<Record<string, Undecide>> = {
785
- 'analytics-and-consent-wiring': (files) => analyseConsentOrder(files).undecided,
918
+ 'staging-noindex-leftover': unless(
919
+ readAnything,
920
+ 'the build tree holds no scannable file at all — "no indexability leftover" would be a ' +
921
+ 'verdict on zero bytes. This check reads .js/.ts/.vue too, so a page-only predicate ' +
922
+ 'would understate what it can normally see.',
923
+ ),
924
+ 'https-enforcement': unless(hasPage, `${NO_MARKUP}, so an insecure asset URL is invisible here rather than absent.`),
925
+ 'per-route-metadata': unless(hasPage, `${NO_MARKUP}, and a head manager sets <title> and description at runtime.`),
926
+ 'image-alternative-text': unless(hasPage, `${NO_MARKUP}, so an <img> with no alt cannot be reached.`),
927
+ 'document-head-basics': unless(hasPage, `${NO_MARKUP}, and lang/charset/viewport come from a shell this reader does not resolve.`),
928
+ 'canonical-and-sitemap-coherence': unless(hasPage, `${NO_MARKUP}, and rel="canonical" is commonly injected at runtime.`),
929
+ 'custom-error-route': unless(
930
+ hasRouteSurface,
931
+ 'no statically inspectable route surface — no HTML page, no sitemap.xml, no routes/ or ' +
932
+ 'pages/ tree, no router module. A 404 route may exist only inside a client bundle, so ' +
933
+ 'its absence as a FILE establishes nothing.',
934
+ ),
935
+ 'required-legal-pages': (files, ctx) => {
936
+ if (!hasRouteSurface(files)) {
937
+ return blind(
938
+ 'no statically inspectable route surface — no HTML page, no sitemap.xml, no ' +
939
+ 'routes/ or pages/ tree, no router module. A legal page matched on its PATH ' +
940
+ 'cannot be found in a tree that publishes no paths.',
941
+ );
942
+ }
943
+ // The declaration arm, and it is D12's silent loss stated precisely.
944
+ // The matcher accepts EITHER the DE pair (Impressum + Datenschutz) or
945
+ // the generic pair (privacy + terms/imprint/legal). A tree carrying only
946
+ // the generic pair passes at every region — including `de`, where an
947
+ // Impressum is owed by name. With no region declared, whether the
948
+ // generic pair discharges the obligation is a question about the
949
+ // CALLER, not about the tree, and the instrument must not answer it.
950
+ const paths = files.map((f) => f.rel.replace(/\\/g, '/'));
951
+ const all = (res: RegExp[]): boolean => res.every((re) => paths.some((p) => re.test(p)));
952
+ const registered = ctx.escalations.some((e) => e.check === 'required-legal-pages');
953
+ if (ctx.region === 'unspecified' && registered && all(LEGAL_GENERIC) && !all(LEGAL_DE)) {
954
+ return blind(
955
+ 'this build carries the generic legal pair (privacy + terms/imprint/legal) and ' +
956
+ 'NOT the DE pair (Impressum + Datenschutz), and no region was declared. The ' +
957
+ 'config registers a region escalation for this check, so the answer depends on ' +
958
+ 'a declaration the caller did not make: pass `--region` or set ' +
959
+ '`web_launch_readiness.region` in .agent-settings.yml.',
960
+ 'declaration-missing',
961
+ );
962
+ }
963
+ return [];
964
+ },
965
+ 'analytics-and-consent-wiring': (files) => {
966
+ if (!hasCode(files)) {
967
+ return blind(
968
+ 'the build tree holds no code file at all (prose is excluded from this pass by ' +
969
+ 'construction), so neither a measurement tag nor a consent gate could be read.',
970
+ );
971
+ }
972
+ return analyseConsentOrder(files).undecided.map((u) => ({ ...u, reason_class: 'instrument-limit' as const }));
973
+ },
786
974
  };
787
975
 
788
976
  /** First line containing `needle`, 1-indexed; 1 when absent (an absence has no line). */
@@ -813,6 +1001,18 @@ export function audit(
813
1001
  siteType: SiteType,
814
1002
  root = REPO_ROOT,
815
1003
  region: Region = 'unspecified',
1004
+ /**
1005
+ * The blind-spot probe table, overridable so a test can DELETE one entry
1006
+ * and observe that check fall back to `passed`.
1007
+ *
1008
+ * That reversal is the whole proof the predicates are narrow: an entry that
1009
+ * cannot be removed without the fixture staying undecided was never doing
1010
+ * the work, and a probe that answers `undecided` on every input is as
1011
+ * useless as one that answers `passed` on every input — only harder to
1012
+ * notice. Same shape as `root` and `region`: defaulted, so no caller
1013
+ * changes.
1014
+ */
1015
+ undecidable: Readonly<Record<string, Undecide>> = UNDECIDABLE,
816
1016
  ): Report {
817
1017
  const { checks, siteTypes, regions, escalations } = loadConfig(root);
818
1018
  if (!siteTypes.includes(siteType)) {
@@ -858,14 +1058,24 @@ export function audit(
858
1058
  impl === undefined
859
1059
  ? scanIndexability(buildDir).hits
860
1060
  : impl(files);
861
- const undecided = UNDECIDABLE[c.id]?.(files) ?? [];
1061
+ const undecided = undecidable[c.id]?.(files, { region, siteType, escalations }) ?? [];
862
1062
  for (const u of undecided) {
863
- unknown.push({ check: c.id, tier, location: `${u.file}:${String(u.line)}`, reason: u.text });
1063
+ unknown.push({
1064
+ check: c.id,
1065
+ tier,
1066
+ location: `${u.file}:${String(u.line)}`,
1067
+ reason: u.text,
1068
+ reason_class: u.reason_class,
1069
+ });
864
1070
  }
865
1071
  if (hits.length === 0 && undecided.length === 0) {
866
1072
  passed.push(c.id);
867
1073
  continue;
868
1074
  }
1075
+ // A surface-blind probe read nothing, so the Impl's hits came from the
1076
+ // same nothing. Reporting them alongside "this establishes nothing"
1077
+ // would print two contradictory sentences from one read.
1078
+ if (undecided.some((u) => u.surface_blind === true)) continue;
869
1079
  for (const h of hits) {
870
1080
  findings.push({
871
1081
  check: c.id,
@@ -877,7 +1087,7 @@ export function audit(
877
1087
  });
878
1088
  }
879
1089
  }
880
- return {
1090
+ const report: Report = {
881
1091
  site_type: siteType,
882
1092
  region,
883
1093
  enabled: enabled(root),
@@ -889,6 +1099,58 @@ export function audit(
889
1099
  escalated,
890
1100
  scanned_files: files.length,
891
1101
  };
1102
+ assertEveryCheckAccounted(report, checks);
1103
+ return report;
1104
+ }
1105
+
1106
+ /** The five report buckets, as a set of check ids each. */
1107
+ export function bucketsOf(r: Report): Readonly<Record<string, ReadonlySet<string>>> {
1108
+ return {
1109
+ findings: new Set(r.findings.map((f) => f.check)),
1110
+ passed: new Set(r.passed),
1111
+ unimplemented: new Set(r.unimplemented),
1112
+ unknown: new Set(r.unknown.map((u) => u.check)),
1113
+ skipped: new Set(r.skipped.map((s) => s.check)),
1114
+ };
1115
+ }
1116
+
1117
+ /**
1118
+ * Every configured check lands in at least one bucket — enforced HERE, not only
1119
+ * in a test, because a test is not present in a consumer's run.
1120
+ *
1121
+ * A COVER, not a partition. A check can be in `findings` AND `unknown` at once:
1122
+ * `audit()` pushes both and only short-circuits to `passed` when both are empty.
1123
+ * Asserting disjointness would therefore red a correct report.
1124
+ *
1125
+ * Compared by check ID rather than by cardinality, because duplicate entries can
1126
+ * make two counts agree while a check has silently vanished — the exact failure
1127
+ * the report's own `passed`/`unimplemented` split exists to prevent. An AI
1128
+ * council (2026-09-04, 2/2) chose the throw over a report field: exposing the
1129
+ * gap as data makes the consumer responsible for noticing what it cannot
1130
+ * reliably detect, and a check that lands nowhere reads as green.
1131
+ *
1132
+ * The error carries the missing ids AND the buckets built so far, so a
1133
+ * diagnosis does not lose the findings that were already computed.
1134
+ */
1135
+ export function assertEveryCheckAccounted(r: Report, checks: readonly CheckDef[]): void {
1136
+ const buckets = bucketsOf(r);
1137
+ const seen = new Set<string>();
1138
+ for (const ids of Object.values(buckets)) for (const id of ids) seen.add(id);
1139
+ const configured = new Set(checks.map((c) => c.id));
1140
+ const missing = [...configured].filter((id) => !seen.has(id)).sort();
1141
+ const stray = [...seen].filter((id) => !configured.has(id)).sort();
1142
+ if (missing.length === 0 && stray.length === 0) return;
1143
+ const where = Object.entries(buckets)
1144
+ .map(([k, v]) => `${k}=[${[...v].sort().join(', ')}]`)
1145
+ .join(' · ');
1146
+ throw new DeadScopeError(
1147
+ 'check_web_launch_readiness',
1148
+ 'the report does not account for every configured check, so some result would have ' +
1149
+ 'been read as clean by omission.' +
1150
+ (missing.length > 0 ? `\n in no bucket: ${missing.join(', ')}` : '') +
1151
+ (stray.length > 0 ? `\n in a bucket but not configured: ${stray.join(', ')}` : '') +
1152
+ `\n buckets built so far: ${where}`,
1153
+ );
892
1154
  }
893
1155
 
894
1156
  const TIER_ORDER: readonly Tier[] = ['critical', 'high', 'medium', 'situational'];
@@ -897,6 +1159,17 @@ const TIER_ORDER: readonly Tier[] = ['critical', 'high', 'medium', 'situational'
897
1159
  export function render(r: Report): string {
898
1160
  const out: string[] = [];
899
1161
  out.push(`site type: ${r.site_type} · region: ${r.region}`);
1162
+ if (r.region === 'unspecified') {
1163
+ // 2.2. The axis that decides SEVERITY is invisible when it is absent,
1164
+ // and an invisible axis reads as "no escalation applies" rather than as
1165
+ // "nobody said". `escalated` is empty on `unspecified` BY CONSTRUCTION,
1166
+ // so silence here is exactly the shape of the lost escalation.
1167
+ out.push(
1168
+ ' region unspecified → DE escalation not active. Every tier below is the base tier; ' +
1169
+ 'declare `--region de` or `web_launch_readiness.region` to raise the ones the ' +
1170
+ 'config registers.',
1171
+ );
1172
+ }
900
1173
  for (const e of r.escalated) {
901
1174
  // Printed BEFORE the findings, because a reader who sees a check at
902
1175
  // CRITICAL on one run and SITUATIONAL on another needs the axis that
@@ -940,16 +1213,23 @@ export function main(argv: string[] = process.argv.slice(2), root = REPO_ROOT):
940
1213
  return i >= 0 ? argv[i + 1] : undefined;
941
1214
  };
942
1215
  const build = arg('--build');
943
- const siteType = arg('--site-type') as SiteType | undefined;
944
- const region = (arg('--region') ?? 'unspecified') as Region;
1216
+ // CLI first, settings second, default last. A flag is an explicit statement
1217
+ // about THIS run; a settings key is a standing declaration about the
1218
+ // project; the default is nobody saying anything, and only the last of the
1219
+ // three is silent about a jurisdiction.
1220
+ const settings = readSettings(root);
1221
+ const siteType = (arg('--site-type') ?? settings.site_type) as SiteType | undefined;
1222
+ const region = (arg('--region') ?? settings.region ?? 'unspecified') as Region;
945
1223
  if (build === undefined || siteType === undefined) {
946
1224
  process.stderr.write(
947
1225
  'usage: check_web_launch_readiness --build <dir> --site-type <type> ' +
948
- '[--region <de|eu|us|unspecified>] [--json]\n',
1226
+ '[--region <de|eu|us|unspecified>] [--json]\n' +
1227
+ ' --site-type and --region also resolve from `web_launch_readiness:` in ' +
1228
+ '.agent-settings.yml; a flag overrides the file.\n',
949
1229
  );
950
1230
  return 2;
951
1231
  }
952
- if (!enabled(root) && !argv.includes('--force')) {
1232
+ if (!settings.enabled && !argv.includes('--force')) {
953
1233
  process.stdout.write(
954
1234
  'web-launch-readiness is DEFAULT-OFF until the pre-registered benchmark returns\n' +
955
1235
  'positive (claim:web-launch-readiness-finds-more, status unbacked). Enable with\n' +
@@ -214,20 +214,30 @@ export const SLOP_RULES: SlopRule[] = [
214
214
  catalogId: "V1",
215
215
  severity: "P1",
216
216
  engines: ["css"],
217
- description: "Colored side-stripe border (border-left/right > 1px) — the #1 AI-UI signature",
217
+ description:
218
+ "Colored accent border > 1px — a side stripe, a four-sided colored border, or a gradient border-image (V1)",
218
219
  message:
219
- "Side-stripe accent border reads as scaffold-default. Remove it, or declare an intentional stripe in DESIGN.md (V1).",
220
- gated: (ctx) => ctx.has("side stripe", "side-stripe", "stripe accent", "accent border"),
220
+ "Colored accent border reads as scaffold-default. Remove it, or declare an intentional accent border in DESIGN.md (V1).",
221
+ gated: (ctx) =>
222
+ ctx.has("side stripe", "side-stripe", "stripe accent", "accent border", "card border"),
221
223
  detect: ({ content }) =>
222
224
  cssBlocks(content)
223
- // a side-stripe is a left/right border with a width > 1px and a color;
224
225
  // exclude semantic blockquote/quote selectors (legitimate left rule)
225
226
  .filter((b) => !/blockquote|\bquote\b|<q>/i.test(b.selector))
226
- .filter((b) =>
227
- /border-(?:left|right)\s*:\s*(?:[2-9]|\d{2,})px\s+\w+\s+(?:#|rgb|hsl|oklch|var\(|[a-z])/i.test(
228
- b.body,
229
- ),
230
- )
227
+ .filter((b) => {
228
+ // A colored value, but never a placeholder one: `transparent` and
229
+ // `currentColor` at > 1px are the layout-reservation and
230
+ // inherit-the-text-color idioms, not an accent decision.
231
+ const COLOR = "(?!transparent|currentcolor|inherit|initial|unset)(?:#|rgb|hsl|oklch|var\\(|[a-z])";
232
+ const WIDTH = "(?:[2-9]|\\d{2,})px";
233
+ // (a) the original side stripe: border-left / border-right
234
+ const stripe = new RegExp(`border-(?:left|right)\\s*:\\s*${WIDTH}\\s+\\w+\\s+${COLOR}`, "i");
235
+ // (b) the current form: a fully colored border on all four sides
236
+ const foursided = new RegExp(`border\\s*:\\s*${WIDTH}\\s+\\w+\\s+${COLOR}`, "i");
237
+ // (c) a gradient border, which carries no width token at all
238
+ const gradient = /border-image\s*:[^;]*(?:linear|conic|radial)-gradient/i;
239
+ return stripe.test(b.body) || foursided.test(b.body) || gradient.test(b.body);
240
+ })
231
241
  .map((b) => ({ line: b.line, snippet: b.selector.slice(0, 80) })),
232
242
  },
233
243
  {