ultra11y 5.33.0 → 5.34.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.opencode/plugins/ultra11y.js +1 -1
- package/package.json +1 -1
- package/scripts/ultra11y.mjs +46 -22
- package/scripts/vendor/wcag-2.2-fr.json +1 -1
- package/scripts/vendor/wcag-2.2-sc-universe.json +1 -1
- package/scripts/vendor/wcag-2.2-techniques.json +1 -1
- package/scripts/vendor/wcag-2.2-text.json +1 -1
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +46 -22
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/scripts/ultra11y.mjs +46 -22
- package/src/adjudicate.ts +20 -17
- package/src/agent-cli.ts +12 -0
- package/src/cli.ts +21 -2
- package/src/data/wcag-universe.json +1 -1
- package/src/llm.ts +9 -0
- package/src/standards/pack.ts +53 -12
- package/src/types.ts +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "ultra11y",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "Automatic accessibility review of the code under change before every commit, push and pull request, plus full WCAG 2.2 AA / RGAA audits and conformance deliverables on demand.",
|
|
13
|
-
"version": "5.
|
|
13
|
+
"version": "5.34.0",
|
|
14
14
|
"author": { "name": "maxgfr" },
|
|
15
15
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
16
16
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.34.0",
|
|
5
5
|
"author": { "name": "maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.34.0",
|
|
5
5
|
"author": { "name": "maxgfr", "url": "https://github.com/maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -27,7 +27,7 @@ import { fileURLToPath } from "node:url";
|
|
|
27
27
|
// Ownership marker: `ultra11y install --opencode` refuses to overwrite a plugin file that
|
|
28
28
|
// does not carry it, so a user's own ultra11y.js is never clobbered.
|
|
29
29
|
const ULTRA11Y_OPENCODE_PLUGIN = "ULTRA11Y_OPENCODE_PLUGIN";
|
|
30
|
-
const ULTRA11Y_PLUGIN_VERSION = "5.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "5.34.0";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.34.0",
|
|
4
4
|
"description": "Audit HTML/CSS/JSX against WCAG 2.2 AA and produce a dated compliance report, or author/review accessible markup (native-HTML-first). A skills.sh agent skill: an install-free static engine (bundled, no npm install) plus the agent's judgment, with check/verify gates against hallucinated non-conformities, and pluggable in-repo standards packs (RGAA France first; Section 508 & EN 301 549 next).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.33.0",
|
package/scripts/ultra11y.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { join as join52, relative as relative5, resolve as resolve16, sep as sep
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "5.
|
|
9
|
+
var VERSION = "5.34.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -2293,7 +2293,7 @@ var wcag_universe_default = {
|
|
|
2293
2293
|
wcagVersion: "2.2",
|
|
2294
2294
|
source: "https://www.w3.org/TR/WCAG22/",
|
|
2295
2295
|
criteriaSource: "https://github.com/w3c/wcag",
|
|
2296
|
-
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-
|
|
2296
|
+
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-25 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
|
|
2297
2297
|
criteria: [
|
|
2298
2298
|
{
|
|
2299
2299
|
id: "1.1.1",
|
|
@@ -50090,16 +50090,28 @@ var MAX_SIBLINGS = 3;
|
|
|
50090
50090
|
function siblingCriteria(pack, id, lang = pack.defaultLocale) {
|
|
50091
50091
|
const c2 = getCriterion(pack, id);
|
|
50092
50092
|
if (!c2) return [];
|
|
50093
|
-
|
|
50093
|
+
return neighbourhood(pack, c2).slice(0, MAX_SIBLINGS).map(({ crit }) => sibling(pack, crit, lang));
|
|
50094
|
+
}
|
|
50095
|
+
var isMechanical = (x) => !x.judgment && (x.appliesTo?.ruleIds ?? []).length > 0;
|
|
50096
|
+
var sibling = (pack, x, lang) => ({
|
|
50097
|
+
id: x.id,
|
|
50098
|
+
// The plain title is the whole argument: « a-t-il une étiquette ? » beside « cette
|
|
50099
|
+
// étiquette est-elle pertinente ? » explains the split better than any prose could.
|
|
50100
|
+
title: localize(pack, x.titlePlain ?? x.title, lang),
|
|
50101
|
+
role: isMechanical(x) ? "mechanical" : "judgment"
|
|
50102
|
+
});
|
|
50103
|
+
function neighbourhood(pack, c2) {
|
|
50094
50104
|
const mine = new Set(c2.wcag ?? []);
|
|
50095
|
-
const
|
|
50096
|
-
return pack.criteria.filter((
|
|
50097
|
-
|
|
50098
|
-
|
|
50099
|
-
|
|
50100
|
-
|
|
50101
|
-
|
|
50102
|
-
|
|
50105
|
+
const mech = isMechanical(c2);
|
|
50106
|
+
return pack.criteria.map((crit) => ({ crit, shared: (crit.wcag ?? []).filter((w) => mine.has(w)).length })).filter(({ crit, shared }) => crit.id !== c2.id && crit.theme === c2.theme && shared > 0 && isMechanical(crit) !== mech).sort((a, b) => b.shared - a.shared || a.crit.id.localeCompare(b.crit.id, void 0, { numeric: true }));
|
|
50107
|
+
}
|
|
50108
|
+
function presupposedCriterion(pack, id, lang = pack.defaultLocale) {
|
|
50109
|
+
const c2 = getCriterion(pack, id);
|
|
50110
|
+
if (!c2 || isMechanical(c2)) return void 0;
|
|
50111
|
+
const ranked = neighbourhood(pack, c2).filter(({ crit }) => isMechanical(crit));
|
|
50112
|
+
const first = ranked[0];
|
|
50113
|
+
if (!first || ranked[1]?.shared === first.shared) return void 0;
|
|
50114
|
+
return sibling(pack, first.crit, lang);
|
|
50103
50115
|
}
|
|
50104
50116
|
function criterionUrl(pack, id) {
|
|
50105
50117
|
return pack.criterionUrl ? pack.criterionUrl.replaceAll("{id}", id) : void 0;
|
|
@@ -59599,16 +59611,12 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59599
59611
|
);
|
|
59600
59612
|
}
|
|
59601
59613
|
if (!isCore(adj.standard) && f.file?.trim()) {
|
|
59602
|
-
const
|
|
59603
|
-
if (
|
|
59604
|
-
|
|
59605
|
-
|
|
59606
|
-
|
|
59607
|
-
|
|
59608
|
-
it.criteriaId,
|
|
59609
|
-
`criterion ${it.criteriaId}: this anchor (${f.file}:${f.line}) is already the engine's non-conformity on ${adj.standard} ${clash.id} \u2014 \xAB ${clash.title} \xBB. ${it.criteriaId} asks the NEXT question about the same subject and presupposes it is there, so on this element it is not non-conformant: it has no subject. Report it on ${clash.id} (the engine already did), or rule ${it.criteriaId} on a different element.`
|
|
59610
|
-
);
|
|
59611
|
-
}
|
|
59614
|
+
const presupposed = presupposedCriterion(loadPack(adj.standard), it.criteriaId);
|
|
59615
|
+
if (presupposed && engineNcAt(anchorKey(f.file, f.line, f.selector ?? "")).has(presupposed.id))
|
|
59616
|
+
blame(
|
|
59617
|
+
it.criteriaId,
|
|
59618
|
+
`criterion ${it.criteriaId}: this anchor (${f.file}:${f.line}) is already the engine's non-conformity on ${adj.standard} ${presupposed.id} \u2014 \xAB ${presupposed.title} \xBB. ${it.criteriaId} asks the NEXT question about the same subject and presupposes it is there, so on this element it is not non-conformant: it has no subject. Report it on ${presupposed.id} (the engine already did), or rule ${it.criteriaId} on a different element.`
|
|
59619
|
+
);
|
|
59612
59620
|
}
|
|
59613
59621
|
toGround(it.criteriaId, { file: f.file, line: f.line, selector: f.selector, snippet: f.snippet });
|
|
59614
59622
|
}
|
|
@@ -60580,6 +60588,7 @@ function applyRawVerdicts(items, verdicts) {
|
|
|
60580
60588
|
var ALLOWED_TOOLS = "Read,Grep,Glob";
|
|
60581
60589
|
var MAX_ATTEMPTS2 = 4;
|
|
60582
60590
|
var DEFAULT_CLI_MODEL = "claude-haiku-4-5-20251001";
|
|
60591
|
+
var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
60583
60592
|
var DEFAULT_TIMEOUT_MS2 = 10 * 6e4;
|
|
60584
60593
|
function realSpawn(argv, input, timeoutMs) {
|
|
60585
60594
|
return new Promise((resolve17, reject) => {
|
|
@@ -60656,6 +60665,7 @@ function cliArgv(opts, items) {
|
|
|
60656
60665
|
opts.model ?? DEFAULT_CLI_MODEL
|
|
60657
60666
|
];
|
|
60658
60667
|
if (opts.maxBudgetUsd !== void 0) argv.push("--max-budget-usd", String(opts.maxBudgetUsd));
|
|
60668
|
+
if (opts.effort) argv.push("--effort", opts.effort);
|
|
60659
60669
|
return argv;
|
|
60660
60670
|
}
|
|
60661
60671
|
function verdictsFromText(text) {
|
|
@@ -60718,6 +60728,7 @@ async function refuteBatchCli(items, prompt, opts, lang = "en") {
|
|
|
60718
60728
|
opts.model ?? DEFAULT_CLI_MODEL
|
|
60719
60729
|
];
|
|
60720
60730
|
if (opts.maxBudgetUsd !== void 0) argv.push("--max-budget-usd", String(opts.maxBudgetUsd));
|
|
60731
|
+
if (opts.effort) argv.push("--effort", opts.effort);
|
|
60721
60732
|
return runCli2(argv, prompt, opts, (env) => {
|
|
60722
60733
|
const structured = env.structured_output?.verdicts;
|
|
60723
60734
|
const raw = Array.isArray(structured) ? structured : verdictsArrayFromText(env.result ?? "");
|
|
@@ -68881,7 +68892,7 @@ Usage:
|
|
|
68881
68892
|
ultra11y orchestrate --run <dir> [--phase adjudicate|verify-report] [--eco] [--list] [--lang auto|en|fr]
|
|
68882
68893
|
ultra11y fix <globs\u2026 | -> [--write] [--iterate] [--changed | --since <ref> | --staged] [--safe] [--include <glob>] [--exclude <glob>] [--ext <list>] [--only <ids>] [--jsx] [--json] [--lang auto|en|fr]
|
|
68883
68894
|
ultra11y init [--hook] [--ci] [--baseline] [--fail-on blocking|major|minor]
|
|
68884
|
-
ultra11y judge --in <audit.json> [--standard <pack>] [--runner api|cli] [--grain batch|criterion] [--max <n>] [--model <id>] [--max-budget-usd <n>] [--timeout <s>] [--out <dir>] [--apply] (adjudicate the manual criteria with a model \u2014 api needs ANTHROPIC_API_KEY; cli runs the Claude Code CLI and takes CLAUDE_CODE_OAUTH_TOKEN too)
|
|
68895
|
+
ultra11y judge --in <audit.json> [--standard <pack>] [--runner api|cli] [--grain batch|criterion] [--max <n>] [--model <id>] [--effort <level>] [--max-budget-usd <n>] [--timeout <s>] [--out <dir>] [--apply] (adjudicate the manual criteria with a model \u2014 api needs ANTHROPIC_API_KEY; cli runs the Claude Code CLI and takes CLAUDE_CODE_OAUTH_TOKEN too)
|
|
68885
68896
|
ultra11y pack check <pack.json> [--guidance <g.json>] [--json] | pack scaffold
|
|
68886
68897
|
ultra11y scan <url|file\u2026> [--runtime auto|local|docker] [--cwd <dir>] [--storage-state <file>] [--no-interact] [--interact-clicks] [--no-snapshot] [--merge <audit.json>] [--out <dir>] [--json]
|
|
68887
68898
|
ultra11y scan --sample [--runtime \u2026] [--cwd <dir>] [--storage-state <file>] [--merge <audit.json>] [--json] (scan the .ultra11yrc.json page sample)
|
|
@@ -69114,6 +69125,8 @@ Options:
|
|
|
69114
69125
|
--provider <id> tickets: auto|github|gitlab|jira. 'auto' reads ULTRA11Y_TICKET_PROVIDER,
|
|
69115
69126
|
then .ultra11yrc.json, then the git remote (Jira is never auto-detected)
|
|
69116
69127
|
--grain <mode> tickets: what one ticket is \u2014 criterion (default) | page | page-criterion | single | file
|
|
69128
|
+
--effort <level> judge (--runner cli): reasoning effort per invocation \u2014 low | medium |
|
|
69129
|
+
high | xhigh | max. Unset leaves the CLI's own default
|
|
69117
69130
|
--transport <t> tickets: auto|cli|rest (mcp: stdio|http). 'auto' prefers the CLI (gh/glab),
|
|
69118
69131
|
falling back to REST when only a token is available. Jira is REST-only
|
|
69119
69132
|
--max-tickets <n> tickets: refuse to file more than n tickets in one run (default 200)
|
|
@@ -69317,6 +69330,7 @@ var VALUE_FLAGS2 = /* @__PURE__ */ new Set([
|
|
|
69317
69330
|
"runner",
|
|
69318
69331
|
"grain",
|
|
69319
69332
|
"max-budget-usd",
|
|
69333
|
+
"effort",
|
|
69320
69334
|
"timeout",
|
|
69321
69335
|
"concurrency",
|
|
69322
69336
|
"out",
|
|
@@ -71452,6 +71466,15 @@ async function cmdJudge(p) {
|
|
|
71452
71466
|
console.error(`ultra11y judge: --grain '${grain}' is not a grain \u2014 expected 'batch' or 'criterion'.`);
|
|
71453
71467
|
return 2;
|
|
71454
71468
|
}
|
|
71469
|
+
const effort = typeof p.flags.effort === "string" ? p.flags.effort : void 0;
|
|
71470
|
+
if (effort !== void 0 && !EFFORT_LEVELS.includes(effort)) {
|
|
71471
|
+
console.error(`ultra11y judge: --effort '${effort}' is not a level \u2014 expected ${EFFORT_LEVELS.map((l) => `'${l}'`).join(", ")}.`);
|
|
71472
|
+
return 2;
|
|
71473
|
+
}
|
|
71474
|
+
if (effort !== void 0 && runner !== "cli") {
|
|
71475
|
+
console.error("ultra11y judge: --effort applies to `--runner cli` only \u2014 the Messages API has no session to set an effort on.");
|
|
71476
|
+
return 2;
|
|
71477
|
+
}
|
|
71455
71478
|
const key2 = typeof p.flags["api-key"] === "string" && p.flags["api-key"] ? p.flags["api-key"] : apiKeyFromEnv();
|
|
71456
71479
|
if (!key2 && runner === "api") {
|
|
71457
71480
|
console.error(
|
|
@@ -71525,6 +71548,7 @@ async function cmdJudge(p) {
|
|
|
71525
71548
|
// requests do. `--concurrency` overrides it for a runner with room.
|
|
71526
71549
|
concurrency: runner === "cli" ? cliConcurrency : void 0,
|
|
71527
71550
|
maxBudgetUsd: Number.isFinite(maxBudgetUsd) ? maxBudgetUsd : void 0,
|
|
71551
|
+
effort,
|
|
71528
71552
|
timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : void 0,
|
|
71529
71553
|
onCost: (usd) => {
|
|
71530
71554
|
spent += usd;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"wcagVersion": "2.2",
|
|
3
3
|
"source": "https://www.w3.org/TR/WCAG22/",
|
|
4
4
|
"criteriaSource": "https://github.com/w3c/wcag",
|
|
5
|
-
"provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-
|
|
5
|
+
"provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-25 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
|
|
6
6
|
"principles": [
|
|
7
7
|
{
|
|
8
8
|
"number": 1,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"source": "https://github.com/w3c/wcag",
|
|
3
3
|
"understanding": "https://www.w3.org/WAI/WCAG22/Understanding/",
|
|
4
4
|
"license": "W3C Document License (code samples: W3C Software License)",
|
|
5
|
-
"fetchedAt": "2026-08-
|
|
5
|
+
"fetchedAt": "2026-08-25",
|
|
6
6
|
"criteria": {
|
|
7
7
|
"1.1.1": {
|
|
8
8
|
"slug": "non-text-content",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"source": "https://github.com/w3c/wcag",
|
|
3
3
|
"license": "W3C Document License",
|
|
4
|
-
"fetchedAt": "2026-08-
|
|
4
|
+
"fetchedAt": "2026-08-25",
|
|
5
5
|
"criteria": {
|
|
6
6
|
"1.1.1": "All non-text content that is presented to the user has a text alternative that serves the equivalent purpose, except for the situations listed below.\nControls, Input:\nIf non-text content is a control or accepts user input, then it has a name that describes its purpose. (Refer to Success Criterion 4.1.2 Name, Role, Value for additional requirements for controls and content that accepts user input.)\nTime-Based Media:\nIf non-text content is time-based media, then text alternatives at least provide descriptive identification of the non-text content. (Refer to Guideline 1.2 for additional requirements for media.)\nTest:\nIf non-text content is a test or exercise that would be invalid if presented in text, then text alternatives at least provide descriptive identification of the non-text content.\nSensory:\nIf non-text content is primarily intended to create a specific sensory experience, then text alternatives at least provide descriptive identification of the non-text content.\nCAPTCHA:\nIf the purpose of non-text content is to confirm that content is being accessed by a person rather than a computer, then text alternatives that identify and describe the purpose of the non-text content are provided, and alternative forms of CAPTCHA using output modes for different types of sensory perception are provided to accommodate different disabilities.\nDecoration, Formatting, Invisible:\nIf non-text content is pure decoration, is used only for visual formatting, or is not presented to users, then it is implemented in a way that it can be ignored by assistive technology.",
|
|
7
7
|
"1.2.1": "For prerecorded audio-only and prerecorded video-only media, the following are true, except when the audio or video is a media alternative for text and is clearly labeled as such:\nPrerecorded Audio-only:\nAn alternative for time-based media is provided that presents equivalent information for prerecorded audio-only content.\nPrerecorded Video-only:\nEither an alternative for time-based media or an audio track is provided that presents equivalent information for prerecorded video-only content.",
|
|
@@ -5,7 +5,7 @@ when_to_use: "Invoke WITHOUT being asked whenever changed front-end code should
|
|
|
5
5
|
allowed-tools: Bash(node ${CLAUDE_SKILL_DIR}/scripts/ultra11y.mjs *), Read, Grep, Glob
|
|
6
6
|
license: MIT
|
|
7
7
|
metadata:
|
|
8
|
-
version: 5.
|
|
8
|
+
version: 5.34.0
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# review-a11y — accessibility review of changed code
|
|
@@ -6,7 +6,7 @@ import { join as join52, relative as relative5, resolve as resolve16, sep as sep
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "5.
|
|
9
|
+
var VERSION = "5.34.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -2293,7 +2293,7 @@ var wcag_universe_default = {
|
|
|
2293
2293
|
wcagVersion: "2.2",
|
|
2294
2294
|
source: "https://www.w3.org/TR/WCAG22/",
|
|
2295
2295
|
criteriaSource: "https://github.com/w3c/wcag",
|
|
2296
|
-
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-
|
|
2296
|
+
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-25 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
|
|
2297
2297
|
criteria: [
|
|
2298
2298
|
{
|
|
2299
2299
|
id: "1.1.1",
|
|
@@ -50090,16 +50090,28 @@ var MAX_SIBLINGS = 3;
|
|
|
50090
50090
|
function siblingCriteria(pack, id, lang = pack.defaultLocale) {
|
|
50091
50091
|
const c2 = getCriterion(pack, id);
|
|
50092
50092
|
if (!c2) return [];
|
|
50093
|
-
|
|
50093
|
+
return neighbourhood(pack, c2).slice(0, MAX_SIBLINGS).map(({ crit }) => sibling(pack, crit, lang));
|
|
50094
|
+
}
|
|
50095
|
+
var isMechanical = (x) => !x.judgment && (x.appliesTo?.ruleIds ?? []).length > 0;
|
|
50096
|
+
var sibling = (pack, x, lang) => ({
|
|
50097
|
+
id: x.id,
|
|
50098
|
+
// The plain title is the whole argument: « a-t-il une étiquette ? » beside « cette
|
|
50099
|
+
// étiquette est-elle pertinente ? » explains the split better than any prose could.
|
|
50100
|
+
title: localize(pack, x.titlePlain ?? x.title, lang),
|
|
50101
|
+
role: isMechanical(x) ? "mechanical" : "judgment"
|
|
50102
|
+
});
|
|
50103
|
+
function neighbourhood(pack, c2) {
|
|
50094
50104
|
const mine = new Set(c2.wcag ?? []);
|
|
50095
|
-
const
|
|
50096
|
-
return pack.criteria.filter((
|
|
50097
|
-
|
|
50098
|
-
|
|
50099
|
-
|
|
50100
|
-
|
|
50101
|
-
|
|
50102
|
-
|
|
50105
|
+
const mech = isMechanical(c2);
|
|
50106
|
+
return pack.criteria.map((crit) => ({ crit, shared: (crit.wcag ?? []).filter((w) => mine.has(w)).length })).filter(({ crit, shared }) => crit.id !== c2.id && crit.theme === c2.theme && shared > 0 && isMechanical(crit) !== mech).sort((a, b) => b.shared - a.shared || a.crit.id.localeCompare(b.crit.id, void 0, { numeric: true }));
|
|
50107
|
+
}
|
|
50108
|
+
function presupposedCriterion(pack, id, lang = pack.defaultLocale) {
|
|
50109
|
+
const c2 = getCriterion(pack, id);
|
|
50110
|
+
if (!c2 || isMechanical(c2)) return void 0;
|
|
50111
|
+
const ranked = neighbourhood(pack, c2).filter(({ crit }) => isMechanical(crit));
|
|
50112
|
+
const first = ranked[0];
|
|
50113
|
+
if (!first || ranked[1]?.shared === first.shared) return void 0;
|
|
50114
|
+
return sibling(pack, first.crit, lang);
|
|
50103
50115
|
}
|
|
50104
50116
|
function criterionUrl(pack, id) {
|
|
50105
50117
|
return pack.criterionUrl ? pack.criterionUrl.replaceAll("{id}", id) : void 0;
|
|
@@ -59599,16 +59611,12 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59599
59611
|
);
|
|
59600
59612
|
}
|
|
59601
59613
|
if (!isCore(adj.standard) && f.file?.trim()) {
|
|
59602
|
-
const
|
|
59603
|
-
if (
|
|
59604
|
-
|
|
59605
|
-
|
|
59606
|
-
|
|
59607
|
-
|
|
59608
|
-
it.criteriaId,
|
|
59609
|
-
`criterion ${it.criteriaId}: this anchor (${f.file}:${f.line}) is already the engine's non-conformity on ${adj.standard} ${clash.id} \u2014 \xAB ${clash.title} \xBB. ${it.criteriaId} asks the NEXT question about the same subject and presupposes it is there, so on this element it is not non-conformant: it has no subject. Report it on ${clash.id} (the engine already did), or rule ${it.criteriaId} on a different element.`
|
|
59610
|
-
);
|
|
59611
|
-
}
|
|
59614
|
+
const presupposed = presupposedCriterion(loadPack(adj.standard), it.criteriaId);
|
|
59615
|
+
if (presupposed && engineNcAt(anchorKey(f.file, f.line, f.selector ?? "")).has(presupposed.id))
|
|
59616
|
+
blame(
|
|
59617
|
+
it.criteriaId,
|
|
59618
|
+
`criterion ${it.criteriaId}: this anchor (${f.file}:${f.line}) is already the engine's non-conformity on ${adj.standard} ${presupposed.id} \u2014 \xAB ${presupposed.title} \xBB. ${it.criteriaId} asks the NEXT question about the same subject and presupposes it is there, so on this element it is not non-conformant: it has no subject. Report it on ${presupposed.id} (the engine already did), or rule ${it.criteriaId} on a different element.`
|
|
59619
|
+
);
|
|
59612
59620
|
}
|
|
59613
59621
|
toGround(it.criteriaId, { file: f.file, line: f.line, selector: f.selector, snippet: f.snippet });
|
|
59614
59622
|
}
|
|
@@ -60580,6 +60588,7 @@ function applyRawVerdicts(items, verdicts) {
|
|
|
60580
60588
|
var ALLOWED_TOOLS = "Read,Grep,Glob";
|
|
60581
60589
|
var MAX_ATTEMPTS2 = 4;
|
|
60582
60590
|
var DEFAULT_CLI_MODEL = "claude-haiku-4-5-20251001";
|
|
60591
|
+
var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
60583
60592
|
var DEFAULT_TIMEOUT_MS2 = 10 * 6e4;
|
|
60584
60593
|
function realSpawn(argv, input, timeoutMs) {
|
|
60585
60594
|
return new Promise((resolve17, reject) => {
|
|
@@ -60656,6 +60665,7 @@ function cliArgv(opts, items) {
|
|
|
60656
60665
|
opts.model ?? DEFAULT_CLI_MODEL
|
|
60657
60666
|
];
|
|
60658
60667
|
if (opts.maxBudgetUsd !== void 0) argv.push("--max-budget-usd", String(opts.maxBudgetUsd));
|
|
60668
|
+
if (opts.effort) argv.push("--effort", opts.effort);
|
|
60659
60669
|
return argv;
|
|
60660
60670
|
}
|
|
60661
60671
|
function verdictsFromText(text) {
|
|
@@ -60718,6 +60728,7 @@ async function refuteBatchCli(items, prompt, opts, lang = "en") {
|
|
|
60718
60728
|
opts.model ?? DEFAULT_CLI_MODEL
|
|
60719
60729
|
];
|
|
60720
60730
|
if (opts.maxBudgetUsd !== void 0) argv.push("--max-budget-usd", String(opts.maxBudgetUsd));
|
|
60731
|
+
if (opts.effort) argv.push("--effort", opts.effort);
|
|
60721
60732
|
return runCli2(argv, prompt, opts, (env) => {
|
|
60722
60733
|
const structured = env.structured_output?.verdicts;
|
|
60723
60734
|
const raw = Array.isArray(structured) ? structured : verdictsArrayFromText(env.result ?? "");
|
|
@@ -68881,7 +68892,7 @@ Usage:
|
|
|
68881
68892
|
ultra11y orchestrate --run <dir> [--phase adjudicate|verify-report] [--eco] [--list] [--lang auto|en|fr]
|
|
68882
68893
|
ultra11y fix <globs\u2026 | -> [--write] [--iterate] [--changed | --since <ref> | --staged] [--safe] [--include <glob>] [--exclude <glob>] [--ext <list>] [--only <ids>] [--jsx] [--json] [--lang auto|en|fr]
|
|
68883
68894
|
ultra11y init [--hook] [--ci] [--baseline] [--fail-on blocking|major|minor]
|
|
68884
|
-
ultra11y judge --in <audit.json> [--standard <pack>] [--runner api|cli] [--grain batch|criterion] [--max <n>] [--model <id>] [--max-budget-usd <n>] [--timeout <s>] [--out <dir>] [--apply] (adjudicate the manual criteria with a model \u2014 api needs ANTHROPIC_API_KEY; cli runs the Claude Code CLI and takes CLAUDE_CODE_OAUTH_TOKEN too)
|
|
68895
|
+
ultra11y judge --in <audit.json> [--standard <pack>] [--runner api|cli] [--grain batch|criterion] [--max <n>] [--model <id>] [--effort <level>] [--max-budget-usd <n>] [--timeout <s>] [--out <dir>] [--apply] (adjudicate the manual criteria with a model \u2014 api needs ANTHROPIC_API_KEY; cli runs the Claude Code CLI and takes CLAUDE_CODE_OAUTH_TOKEN too)
|
|
68885
68896
|
ultra11y pack check <pack.json> [--guidance <g.json>] [--json] | pack scaffold
|
|
68886
68897
|
ultra11y scan <url|file\u2026> [--runtime auto|local|docker] [--cwd <dir>] [--storage-state <file>] [--no-interact] [--interact-clicks] [--no-snapshot] [--merge <audit.json>] [--out <dir>] [--json]
|
|
68887
68898
|
ultra11y scan --sample [--runtime \u2026] [--cwd <dir>] [--storage-state <file>] [--merge <audit.json>] [--json] (scan the .ultra11yrc.json page sample)
|
|
@@ -69114,6 +69125,8 @@ Options:
|
|
|
69114
69125
|
--provider <id> tickets: auto|github|gitlab|jira. 'auto' reads ULTRA11Y_TICKET_PROVIDER,
|
|
69115
69126
|
then .ultra11yrc.json, then the git remote (Jira is never auto-detected)
|
|
69116
69127
|
--grain <mode> tickets: what one ticket is \u2014 criterion (default) | page | page-criterion | single | file
|
|
69128
|
+
--effort <level> judge (--runner cli): reasoning effort per invocation \u2014 low | medium |
|
|
69129
|
+
high | xhigh | max. Unset leaves the CLI's own default
|
|
69117
69130
|
--transport <t> tickets: auto|cli|rest (mcp: stdio|http). 'auto' prefers the CLI (gh/glab),
|
|
69118
69131
|
falling back to REST when only a token is available. Jira is REST-only
|
|
69119
69132
|
--max-tickets <n> tickets: refuse to file more than n tickets in one run (default 200)
|
|
@@ -69317,6 +69330,7 @@ var VALUE_FLAGS2 = /* @__PURE__ */ new Set([
|
|
|
69317
69330
|
"runner",
|
|
69318
69331
|
"grain",
|
|
69319
69332
|
"max-budget-usd",
|
|
69333
|
+
"effort",
|
|
69320
69334
|
"timeout",
|
|
69321
69335
|
"concurrency",
|
|
69322
69336
|
"out",
|
|
@@ -71452,6 +71466,15 @@ async function cmdJudge(p) {
|
|
|
71452
71466
|
console.error(`ultra11y judge: --grain '${grain}' is not a grain \u2014 expected 'batch' or 'criterion'.`);
|
|
71453
71467
|
return 2;
|
|
71454
71468
|
}
|
|
71469
|
+
const effort = typeof p.flags.effort === "string" ? p.flags.effort : void 0;
|
|
71470
|
+
if (effort !== void 0 && !EFFORT_LEVELS.includes(effort)) {
|
|
71471
|
+
console.error(`ultra11y judge: --effort '${effort}' is not a level \u2014 expected ${EFFORT_LEVELS.map((l) => `'${l}'`).join(", ")}.`);
|
|
71472
|
+
return 2;
|
|
71473
|
+
}
|
|
71474
|
+
if (effort !== void 0 && runner !== "cli") {
|
|
71475
|
+
console.error("ultra11y judge: --effort applies to `--runner cli` only \u2014 the Messages API has no session to set an effort on.");
|
|
71476
|
+
return 2;
|
|
71477
|
+
}
|
|
71455
71478
|
const key2 = typeof p.flags["api-key"] === "string" && p.flags["api-key"] ? p.flags["api-key"] : apiKeyFromEnv();
|
|
71456
71479
|
if (!key2 && runner === "api") {
|
|
71457
71480
|
console.error(
|
|
@@ -71525,6 +71548,7 @@ async function cmdJudge(p) {
|
|
|
71525
71548
|
// requests do. `--concurrency` overrides it for a runner with room.
|
|
71526
71549
|
concurrency: runner === "cli" ? cliConcurrency : void 0,
|
|
71527
71550
|
maxBudgetUsd: Number.isFinite(maxBudgetUsd) ? maxBudgetUsd : void 0,
|
|
71551
|
+
effort,
|
|
71528
71552
|
timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : void 0,
|
|
71529
71553
|
onCost: (usd) => {
|
|
71530
71554
|
spent += usd;
|
package/skills/ultra11y/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: "Use to AUDIT existing HTML/CSS/JSX against WCAG 2.2 AA accessibili
|
|
|
4
4
|
when_to_use: "Invoke when the user asks for an accessibility AUDIT or a formal deliverable: audit a repo, site or page against WCAG 2.2 AA or a country standard (RGAA, Section 508, EN 301 549); produce a dated conformance report, a criterion grid, a PRD backlog, or tickets (GitHub/GitLab/Jira); author accessible markup from scratch; wire the repo gate (init --hook/--ci) or a rendered-DOM capture pipeline. For a review of the code UNDER CHANGE (diff, branch, PR), use the `review-a11y` skill instead."
|
|
5
5
|
license: MIT
|
|
6
6
|
metadata:
|
|
7
|
-
version: 5.
|
|
7
|
+
version: 5.34.0
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# ultra11y — audit WCAG 2.2 AA and write accessible markup
|
|
@@ -6,7 +6,7 @@ import { join as join52, relative as relative5, resolve as resolve16, sep as sep
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "5.
|
|
9
|
+
var VERSION = "5.34.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -2293,7 +2293,7 @@ var wcag_universe_default = {
|
|
|
2293
2293
|
wcagVersion: "2.2",
|
|
2294
2294
|
source: "https://www.w3.org/TR/WCAG22/",
|
|
2295
2295
|
criteriaSource: "https://github.com/w3c/wcag",
|
|
2296
|
-
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-
|
|
2296
|
+
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-25 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
|
|
2297
2297
|
criteria: [
|
|
2298
2298
|
{
|
|
2299
2299
|
id: "1.1.1",
|
|
@@ -50090,16 +50090,28 @@ var MAX_SIBLINGS = 3;
|
|
|
50090
50090
|
function siblingCriteria(pack, id, lang = pack.defaultLocale) {
|
|
50091
50091
|
const c2 = getCriterion(pack, id);
|
|
50092
50092
|
if (!c2) return [];
|
|
50093
|
-
|
|
50093
|
+
return neighbourhood(pack, c2).slice(0, MAX_SIBLINGS).map(({ crit }) => sibling(pack, crit, lang));
|
|
50094
|
+
}
|
|
50095
|
+
var isMechanical = (x) => !x.judgment && (x.appliesTo?.ruleIds ?? []).length > 0;
|
|
50096
|
+
var sibling = (pack, x, lang) => ({
|
|
50097
|
+
id: x.id,
|
|
50098
|
+
// The plain title is the whole argument: « a-t-il une étiquette ? » beside « cette
|
|
50099
|
+
// étiquette est-elle pertinente ? » explains the split better than any prose could.
|
|
50100
|
+
title: localize(pack, x.titlePlain ?? x.title, lang),
|
|
50101
|
+
role: isMechanical(x) ? "mechanical" : "judgment"
|
|
50102
|
+
});
|
|
50103
|
+
function neighbourhood(pack, c2) {
|
|
50094
50104
|
const mine = new Set(c2.wcag ?? []);
|
|
50095
|
-
const
|
|
50096
|
-
return pack.criteria.filter((
|
|
50097
|
-
|
|
50098
|
-
|
|
50099
|
-
|
|
50100
|
-
|
|
50101
|
-
|
|
50102
|
-
|
|
50105
|
+
const mech = isMechanical(c2);
|
|
50106
|
+
return pack.criteria.map((crit) => ({ crit, shared: (crit.wcag ?? []).filter((w) => mine.has(w)).length })).filter(({ crit, shared }) => crit.id !== c2.id && crit.theme === c2.theme && shared > 0 && isMechanical(crit) !== mech).sort((a, b) => b.shared - a.shared || a.crit.id.localeCompare(b.crit.id, void 0, { numeric: true }));
|
|
50107
|
+
}
|
|
50108
|
+
function presupposedCriterion(pack, id, lang = pack.defaultLocale) {
|
|
50109
|
+
const c2 = getCriterion(pack, id);
|
|
50110
|
+
if (!c2 || isMechanical(c2)) return void 0;
|
|
50111
|
+
const ranked = neighbourhood(pack, c2).filter(({ crit }) => isMechanical(crit));
|
|
50112
|
+
const first = ranked[0];
|
|
50113
|
+
if (!first || ranked[1]?.shared === first.shared) return void 0;
|
|
50114
|
+
return sibling(pack, first.crit, lang);
|
|
50103
50115
|
}
|
|
50104
50116
|
function criterionUrl(pack, id) {
|
|
50105
50117
|
return pack.criterionUrl ? pack.criterionUrl.replaceAll("{id}", id) : void 0;
|
|
@@ -59599,16 +59611,12 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59599
59611
|
);
|
|
59600
59612
|
}
|
|
59601
59613
|
if (!isCore(adj.standard) && f.file?.trim()) {
|
|
59602
|
-
const
|
|
59603
|
-
if (
|
|
59604
|
-
|
|
59605
|
-
|
|
59606
|
-
|
|
59607
|
-
|
|
59608
|
-
it.criteriaId,
|
|
59609
|
-
`criterion ${it.criteriaId}: this anchor (${f.file}:${f.line}) is already the engine's non-conformity on ${adj.standard} ${clash.id} \u2014 \xAB ${clash.title} \xBB. ${it.criteriaId} asks the NEXT question about the same subject and presupposes it is there, so on this element it is not non-conformant: it has no subject. Report it on ${clash.id} (the engine already did), or rule ${it.criteriaId} on a different element.`
|
|
59610
|
-
);
|
|
59611
|
-
}
|
|
59614
|
+
const presupposed = presupposedCriterion(loadPack(adj.standard), it.criteriaId);
|
|
59615
|
+
if (presupposed && engineNcAt(anchorKey(f.file, f.line, f.selector ?? "")).has(presupposed.id))
|
|
59616
|
+
blame(
|
|
59617
|
+
it.criteriaId,
|
|
59618
|
+
`criterion ${it.criteriaId}: this anchor (${f.file}:${f.line}) is already the engine's non-conformity on ${adj.standard} ${presupposed.id} \u2014 \xAB ${presupposed.title} \xBB. ${it.criteriaId} asks the NEXT question about the same subject and presupposes it is there, so on this element it is not non-conformant: it has no subject. Report it on ${presupposed.id} (the engine already did), or rule ${it.criteriaId} on a different element.`
|
|
59619
|
+
);
|
|
59612
59620
|
}
|
|
59613
59621
|
toGround(it.criteriaId, { file: f.file, line: f.line, selector: f.selector, snippet: f.snippet });
|
|
59614
59622
|
}
|
|
@@ -60580,6 +60588,7 @@ function applyRawVerdicts(items, verdicts) {
|
|
|
60580
60588
|
var ALLOWED_TOOLS = "Read,Grep,Glob";
|
|
60581
60589
|
var MAX_ATTEMPTS2 = 4;
|
|
60582
60590
|
var DEFAULT_CLI_MODEL = "claude-haiku-4-5-20251001";
|
|
60591
|
+
var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
60583
60592
|
var DEFAULT_TIMEOUT_MS2 = 10 * 6e4;
|
|
60584
60593
|
function realSpawn(argv, input, timeoutMs) {
|
|
60585
60594
|
return new Promise((resolve17, reject) => {
|
|
@@ -60656,6 +60665,7 @@ function cliArgv(opts, items) {
|
|
|
60656
60665
|
opts.model ?? DEFAULT_CLI_MODEL
|
|
60657
60666
|
];
|
|
60658
60667
|
if (opts.maxBudgetUsd !== void 0) argv.push("--max-budget-usd", String(opts.maxBudgetUsd));
|
|
60668
|
+
if (opts.effort) argv.push("--effort", opts.effort);
|
|
60659
60669
|
return argv;
|
|
60660
60670
|
}
|
|
60661
60671
|
function verdictsFromText(text) {
|
|
@@ -60718,6 +60728,7 @@ async function refuteBatchCli(items, prompt, opts, lang = "en") {
|
|
|
60718
60728
|
opts.model ?? DEFAULT_CLI_MODEL
|
|
60719
60729
|
];
|
|
60720
60730
|
if (opts.maxBudgetUsd !== void 0) argv.push("--max-budget-usd", String(opts.maxBudgetUsd));
|
|
60731
|
+
if (opts.effort) argv.push("--effort", opts.effort);
|
|
60721
60732
|
return runCli2(argv, prompt, opts, (env) => {
|
|
60722
60733
|
const structured = env.structured_output?.verdicts;
|
|
60723
60734
|
const raw = Array.isArray(structured) ? structured : verdictsArrayFromText(env.result ?? "");
|
|
@@ -68881,7 +68892,7 @@ Usage:
|
|
|
68881
68892
|
ultra11y orchestrate --run <dir> [--phase adjudicate|verify-report] [--eco] [--list] [--lang auto|en|fr]
|
|
68882
68893
|
ultra11y fix <globs\u2026 | -> [--write] [--iterate] [--changed | --since <ref> | --staged] [--safe] [--include <glob>] [--exclude <glob>] [--ext <list>] [--only <ids>] [--jsx] [--json] [--lang auto|en|fr]
|
|
68883
68894
|
ultra11y init [--hook] [--ci] [--baseline] [--fail-on blocking|major|minor]
|
|
68884
|
-
ultra11y judge --in <audit.json> [--standard <pack>] [--runner api|cli] [--grain batch|criterion] [--max <n>] [--model <id>] [--max-budget-usd <n>] [--timeout <s>] [--out <dir>] [--apply] (adjudicate the manual criteria with a model \u2014 api needs ANTHROPIC_API_KEY; cli runs the Claude Code CLI and takes CLAUDE_CODE_OAUTH_TOKEN too)
|
|
68895
|
+
ultra11y judge --in <audit.json> [--standard <pack>] [--runner api|cli] [--grain batch|criterion] [--max <n>] [--model <id>] [--effort <level>] [--max-budget-usd <n>] [--timeout <s>] [--out <dir>] [--apply] (adjudicate the manual criteria with a model \u2014 api needs ANTHROPIC_API_KEY; cli runs the Claude Code CLI and takes CLAUDE_CODE_OAUTH_TOKEN too)
|
|
68885
68896
|
ultra11y pack check <pack.json> [--guidance <g.json>] [--json] | pack scaffold
|
|
68886
68897
|
ultra11y scan <url|file\u2026> [--runtime auto|local|docker] [--cwd <dir>] [--storage-state <file>] [--no-interact] [--interact-clicks] [--no-snapshot] [--merge <audit.json>] [--out <dir>] [--json]
|
|
68887
68898
|
ultra11y scan --sample [--runtime \u2026] [--cwd <dir>] [--storage-state <file>] [--merge <audit.json>] [--json] (scan the .ultra11yrc.json page sample)
|
|
@@ -69114,6 +69125,8 @@ Options:
|
|
|
69114
69125
|
--provider <id> tickets: auto|github|gitlab|jira. 'auto' reads ULTRA11Y_TICKET_PROVIDER,
|
|
69115
69126
|
then .ultra11yrc.json, then the git remote (Jira is never auto-detected)
|
|
69116
69127
|
--grain <mode> tickets: what one ticket is \u2014 criterion (default) | page | page-criterion | single | file
|
|
69128
|
+
--effort <level> judge (--runner cli): reasoning effort per invocation \u2014 low | medium |
|
|
69129
|
+
high | xhigh | max. Unset leaves the CLI's own default
|
|
69117
69130
|
--transport <t> tickets: auto|cli|rest (mcp: stdio|http). 'auto' prefers the CLI (gh/glab),
|
|
69118
69131
|
falling back to REST when only a token is available. Jira is REST-only
|
|
69119
69132
|
--max-tickets <n> tickets: refuse to file more than n tickets in one run (default 200)
|
|
@@ -69317,6 +69330,7 @@ var VALUE_FLAGS2 = /* @__PURE__ */ new Set([
|
|
|
69317
69330
|
"runner",
|
|
69318
69331
|
"grain",
|
|
69319
69332
|
"max-budget-usd",
|
|
69333
|
+
"effort",
|
|
69320
69334
|
"timeout",
|
|
69321
69335
|
"concurrency",
|
|
69322
69336
|
"out",
|
|
@@ -71452,6 +71466,15 @@ async function cmdJudge(p) {
|
|
|
71452
71466
|
console.error(`ultra11y judge: --grain '${grain}' is not a grain \u2014 expected 'batch' or 'criterion'.`);
|
|
71453
71467
|
return 2;
|
|
71454
71468
|
}
|
|
71469
|
+
const effort = typeof p.flags.effort === "string" ? p.flags.effort : void 0;
|
|
71470
|
+
if (effort !== void 0 && !EFFORT_LEVELS.includes(effort)) {
|
|
71471
|
+
console.error(`ultra11y judge: --effort '${effort}' is not a level \u2014 expected ${EFFORT_LEVELS.map((l) => `'${l}'`).join(", ")}.`);
|
|
71472
|
+
return 2;
|
|
71473
|
+
}
|
|
71474
|
+
if (effort !== void 0 && runner !== "cli") {
|
|
71475
|
+
console.error("ultra11y judge: --effort applies to `--runner cli` only \u2014 the Messages API has no session to set an effort on.");
|
|
71476
|
+
return 2;
|
|
71477
|
+
}
|
|
71455
71478
|
const key2 = typeof p.flags["api-key"] === "string" && p.flags["api-key"] ? p.flags["api-key"] : apiKeyFromEnv();
|
|
71456
71479
|
if (!key2 && runner === "api") {
|
|
71457
71480
|
console.error(
|
|
@@ -71525,6 +71548,7 @@ async function cmdJudge(p) {
|
|
|
71525
71548
|
// requests do. `--concurrency` overrides it for a runner with room.
|
|
71526
71549
|
concurrency: runner === "cli" ? cliConcurrency : void 0,
|
|
71527
71550
|
maxBudgetUsd: Number.isFinite(maxBudgetUsd) ? maxBudgetUsd : void 0,
|
|
71551
|
+
effort,
|
|
71528
71552
|
timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : void 0,
|
|
71529
71553
|
onCost: (usd) => {
|
|
71530
71554
|
spent += usd;
|
package/src/adjudicate.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
localize,
|
|
37
37
|
resolveGlossary,
|
|
38
38
|
siblingCriteria,
|
|
39
|
+
presupposedCriterion,
|
|
39
40
|
type StandardPack,
|
|
40
41
|
type PackCriterion,
|
|
41
42
|
} from "./standards/index.js";
|
|
@@ -992,27 +993,29 @@ export function applyAdjudication(
|
|
|
992
993
|
// really does ground.
|
|
993
994
|
//
|
|
994
995
|
// Narrow on purpose, and this is the one check here that could refuse a true finding.
|
|
995
|
-
// It fires only when all three hold: the criterion under verdict
|
|
996
|
-
// neighbour (`
|
|
997
|
-
//
|
|
998
|
-
//
|
|
999
|
-
//
|
|
1000
|
-
//
|
|
1001
|
-
//
|
|
996
|
+
// It fires only when all three hold: the criterion under verdict PRESUPPOSES a
|
|
997
|
+
// mechanical neighbour's subject (`presupposedCriterion`), the engine has already ruled
|
|
998
|
+
// that neighbour non-conformant, and the anchor is literally the same file, line and
|
|
999
|
+
// selector.
|
|
1000
|
+
//
|
|
1001
|
+
// It read `siblingCriteria` before, and that was too wide by exactly the margin that
|
|
1002
|
+
// matters. The neighbourhood is a reading aid — up to three criteria, no claim that any
|
|
1003
|
+
// of them comes first — so 11.4 (« l'étiquette est-elle accolée à son champ ? ») counted
|
|
1004
|
+
// 11.5 (« les champs de même nature sont-ils regroupés ? ») as a question it
|
|
1005
|
+
// presupposes, and a radio group the engine ruled ungrouped became immune to 11.4 on
|
|
1006
|
+
// the same control. Two defects on one element are not one defect charged twice.
|
|
1007
|
+
// `presupposedCriterion` answers only where the pack singles ONE neighbour out and
|
|
1008
|
+
// stands down on a tie, which keeps 11.2/11.1 and 8.6/8.5 and drops the guesses.
|
|
1002
1009
|
//
|
|
1003
1010
|
// Refused per verdict like everything else: the criterion returns to « to assess »
|
|
1004
1011
|
// carrying the reason, naming the neighbour, so the next pass can file it correctly.
|
|
1005
1012
|
if (!isCore(adj.standard) && f.file?.trim()) {
|
|
1006
|
-
const
|
|
1007
|
-
if (
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
it.criteriaId,
|
|
1013
|
-
`criterion ${it.criteriaId}: this anchor (${f.file}:${f.line}) is already the engine's non-conformity on ${adj.standard} ${clash.id} — « ${clash.title} ». ${it.criteriaId} asks the NEXT question about the same subject and presupposes it is there, so on this element it is not non-conformant: it has no subject. Report it on ${clash.id} (the engine already did), or rule ${it.criteriaId} on a different element.`,
|
|
1014
|
-
);
|
|
1015
|
-
}
|
|
1013
|
+
const presupposed = presupposedCriterion(loadPack(adj.standard), it.criteriaId);
|
|
1014
|
+
if (presupposed && engineNcAt(anchorKey(f.file, f.line, f.selector ?? "")).has(presupposed.id))
|
|
1015
|
+
blame(
|
|
1016
|
+
it.criteriaId,
|
|
1017
|
+
`criterion ${it.criteriaId}: this anchor (${f.file}:${f.line}) is already the engine's non-conformity on ${adj.standard} ${presupposed.id} — « ${presupposed.title} ». ${it.criteriaId} asks the NEXT question about the same subject and presupposes it is there, so on this element it is not non-conformant: it has no subject. Report it on ${presupposed.id} (the engine already did), or rule ${it.criteriaId} on a different element.`,
|
|
1018
|
+
);
|
|
1016
1019
|
}
|
|
1017
1020
|
toGround(it.criteriaId, { file: f.file, line: f.line, selector: f.selector, snippet: f.snippet });
|
|
1018
1021
|
}
|
package/src/agent-cli.ts
CHANGED
|
@@ -39,6 +39,12 @@ const MAX_ATTEMPTS = 4;
|
|
|
39
39
|
* states a model rather than inheriting one. */
|
|
40
40
|
export const DEFAULT_CLI_MODEL = "claude-haiku-4-5-20251001";
|
|
41
41
|
|
|
42
|
+
/** The levels `claude --effort` accepts. Enumerated here so a caller's typo is refused with a
|
|
43
|
+
* message instead of forwarded: this CLI treats an unrecognised value the way it treats an
|
|
44
|
+
* unrecognised flag — silently — and a ceiling that is silently nothing is the exact failure
|
|
45
|
+
* `--max-turns` already cost this repo once. */
|
|
46
|
+
export const EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
47
|
+
|
|
42
48
|
/** What `claude -p --output-format json` puts on stdout. Only the fields this reads. */
|
|
43
49
|
interface CliEnvelope {
|
|
44
50
|
/** Where `--json-schema` puts the validated object. Present since 2.1.x; `result` carries
|
|
@@ -168,6 +174,9 @@ export function cliArgv(opts: LlmOptions, items: AdjudicationItem[]): string[] {
|
|
|
168
174
|
// a turn budget and be an unbounded run. The bounds that are real: `--max-budget-usd`, which
|
|
169
175
|
// the CLI documents, and the wall-clock kill in `judgeBatchCli`, which is ours.
|
|
170
176
|
if (opts.maxBudgetUsd !== undefined) argv.push("--max-budget-usd", String(opts.maxBudgetUsd));
|
|
177
|
+
// `--effort` IS documented by this CLI, unlike `--max-turns` above — so it is passed, and a
|
|
178
|
+
// caller can raise how hard the model thinks without also having to change the model.
|
|
179
|
+
if (opts.effort) argv.push("--effort", opts.effort);
|
|
171
180
|
return argv;
|
|
172
181
|
}
|
|
173
182
|
|
|
@@ -274,6 +283,9 @@ export async function refuteBatchCli(items: VerifyItem[], prompt: string, opts:
|
|
|
274
283
|
opts.model ?? DEFAULT_CLI_MODEL,
|
|
275
284
|
];
|
|
276
285
|
if (opts.maxBudgetUsd !== undefined) argv.push("--max-budget-usd", String(opts.maxBudgetUsd));
|
|
286
|
+
// `--effort` IS documented by this CLI, unlike `--max-turns` above — so it is passed, and a
|
|
287
|
+
// caller can raise how hard the model thinks without also having to change the model.
|
|
288
|
+
if (opts.effort) argv.push("--effort", opts.effort);
|
|
277
289
|
return runCli(argv, prompt, opts, (env) => {
|
|
278
290
|
const structured = (env.structured_output as { verdicts?: unknown } | undefined)?.verdicts;
|
|
279
291
|
const raw = Array.isArray(structured) ? structured : verdictsArrayFromText(env.result ?? "");
|
package/src/cli.ts
CHANGED
|
@@ -64,7 +64,7 @@ import {
|
|
|
64
64
|
type AdjudicationFile,
|
|
65
65
|
} from "./adjudicate.js";
|
|
66
66
|
import { entriesFrom, isLedger, ledgerPath, mergeLedger, readLedger, replayLedger, unreadableCaptures, type VerdictLedger, writeLedger } from "./ledger.js";
|
|
67
|
-
import { DEFAULT_CLI_MODEL, judgeBatchCli, refuteBatchCli } from "./agent-cli.js";
|
|
67
|
+
import { DEFAULT_CLI_MODEL, EFFORT_LEVELS, judgeBatchCli, refuteBatchCli } from "./agent-cli.js";
|
|
68
68
|
import { BATCH_SIZE, apiKeyFromEnv, applyRawVerdicts, judgeAll, modelFromEnv, type LlmOptions } from "./llm.js";
|
|
69
69
|
import { runScan, runScanMany, runCrawlScan, runSampleScan, mergeDynamic, mergeSnapshotAudit, cleanDynamic, dockerAvailable } from "./scan.js";
|
|
70
70
|
import { runScanLocal, runScanManyLocal, runCrawlScanLocal, runSampleScanLocal, localAvailable, localTierStatus } from "./scan-local.js";
|
|
@@ -142,7 +142,7 @@ Usage:
|
|
|
142
142
|
ultra11y orchestrate --run <dir> [--phase adjudicate|verify-report] [--eco] [--list] [--lang auto|en|fr]
|
|
143
143
|
ultra11y fix <globs… | -> [--write] [--iterate] [--changed | --since <ref> | --staged] [--safe] [--include <glob>] [--exclude <glob>] [--ext <list>] [--only <ids>] [--jsx] [--json] [--lang auto|en|fr]
|
|
144
144
|
ultra11y init [--hook] [--ci] [--baseline] [--fail-on blocking|major|minor]
|
|
145
|
-
ultra11y judge --in <audit.json> [--standard <pack>] [--runner api|cli] [--grain batch|criterion] [--max <n>] [--model <id>] [--max-budget-usd <n>] [--timeout <s>] [--out <dir>] [--apply] (adjudicate the manual criteria with a model — api needs ANTHROPIC_API_KEY; cli runs the Claude Code CLI and takes CLAUDE_CODE_OAUTH_TOKEN too)
|
|
145
|
+
ultra11y judge --in <audit.json> [--standard <pack>] [--runner api|cli] [--grain batch|criterion] [--max <n>] [--model <id>] [--effort <level>] [--max-budget-usd <n>] [--timeout <s>] [--out <dir>] [--apply] (adjudicate the manual criteria with a model — api needs ANTHROPIC_API_KEY; cli runs the Claude Code CLI and takes CLAUDE_CODE_OAUTH_TOKEN too)
|
|
146
146
|
ultra11y pack check <pack.json> [--guidance <g.json>] [--json] | pack scaffold
|
|
147
147
|
ultra11y scan <url|file…> [--runtime auto|local|docker] [--cwd <dir>] [--storage-state <file>] [--no-interact] [--interact-clicks] [--no-snapshot] [--merge <audit.json>] [--out <dir>] [--json]
|
|
148
148
|
ultra11y scan --sample [--runtime …] [--cwd <dir>] [--storage-state <file>] [--merge <audit.json>] [--json] (scan the .ultra11yrc.json page sample)
|
|
@@ -375,6 +375,8 @@ Options:
|
|
|
375
375
|
--provider <id> tickets: auto|github|gitlab|jira. 'auto' reads ULTRA11Y_TICKET_PROVIDER,
|
|
376
376
|
then .ultra11yrc.json, then the git remote (Jira is never auto-detected)
|
|
377
377
|
--grain <mode> tickets: what one ticket is — criterion (default) | page | page-criterion | single | file
|
|
378
|
+
--effort <level> judge (--runner cli): reasoning effort per invocation — low | medium |
|
|
379
|
+
high | xhigh | max. Unset leaves the CLI's own default
|
|
378
380
|
--transport <t> tickets: auto|cli|rest (mcp: stdio|http). 'auto' prefers the CLI (gh/glab),
|
|
379
381
|
falling back to REST when only a token is available. Jira is REST-only
|
|
380
382
|
--max-tickets <n> tickets: refuse to file more than n tickets in one run (default 200)
|
|
@@ -582,6 +584,7 @@ const VALUE_FLAGS = new Set([
|
|
|
582
584
|
"runner",
|
|
583
585
|
"grain",
|
|
584
586
|
"max-budget-usd",
|
|
587
|
+
"effort",
|
|
585
588
|
"timeout",
|
|
586
589
|
"concurrency",
|
|
587
590
|
"out",
|
|
@@ -3561,6 +3564,21 @@ async function cmdJudge(p: ParsedArgs): Promise<number> {
|
|
|
3561
3564
|
return 2;
|
|
3562
3565
|
}
|
|
3563
3566
|
|
|
3567
|
+
// HOW HARD THE MODEL IS ASKED TO THINK, which on this worklist is not a detail: what reaches
|
|
3568
|
+
// this tier is what no engine could decide. Validated rather than forwarded, because the CLI
|
|
3569
|
+
// SWALLOWS AN UNKNOWN VALUE the same way it swallows an unknown flag — `--effort hgih` would
|
|
3570
|
+
// read as a setting and be nothing at all, which is the failure mode this repo already ate
|
|
3571
|
+
// once with `--max-turns`. Refused here, loudly, while the message can still reach a human.
|
|
3572
|
+
const effort = typeof p.flags.effort === "string" ? (p.flags.effort as string) : undefined;
|
|
3573
|
+
if (effort !== undefined && !EFFORT_LEVELS.includes(effort)) {
|
|
3574
|
+
console.error(`ultra11y judge: --effort '${effort}' is not a level — expected ${EFFORT_LEVELS.map((l) => `'${l}'`).join(", ")}.`);
|
|
3575
|
+
return 2;
|
|
3576
|
+
}
|
|
3577
|
+
if (effort !== undefined && runner !== "cli") {
|
|
3578
|
+
console.error("ultra11y judge: --effort applies to `--runner cli` only — the Messages API has no session to set an effort on.");
|
|
3579
|
+
return 2;
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3564
3582
|
const key = typeof p.flags["api-key"] === "string" && p.flags["api-key"] ? (p.flags["api-key"] as string) : apiKeyFromEnv();
|
|
3565
3583
|
if (!key && runner === "api") {
|
|
3566
3584
|
console.error(
|
|
@@ -3684,6 +3702,7 @@ async function cmdJudge(p: ParsedArgs): Promise<number> {
|
|
|
3684
3702
|
// requests do. `--concurrency` overrides it for a runner with room.
|
|
3685
3703
|
concurrency: runner === "cli" ? cliConcurrency : undefined,
|
|
3686
3704
|
maxBudgetUsd: Number.isFinite(maxBudgetUsd) ? maxBudgetUsd : undefined,
|
|
3705
|
+
effort,
|
|
3687
3706
|
timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : undefined,
|
|
3688
3707
|
onCost: (usd) => {
|
|
3689
3708
|
spent += usd;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"wcagVersion": "2.2",
|
|
3
3
|
"source": "https://www.w3.org/TR/WCAG22/",
|
|
4
4
|
"criteriaSource": "https://github.com/w3c/wcag",
|
|
5
|
-
"provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-
|
|
5
|
+
"provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-25 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
|
|
6
6
|
"criteria": [
|
|
7
7
|
{
|
|
8
8
|
"id": "1.1.1",
|
package/src/llm.ts
CHANGED
|
@@ -71,6 +71,15 @@ export interface LlmOptions {
|
|
|
71
71
|
/** Dollar ceiling handed to the CLI backend — the bound that survives whatever a turn
|
|
72
72
|
* happens to cost, which is what a CI job actually wants to cap. */
|
|
73
73
|
maxBudgetUsd?: number;
|
|
74
|
+
/** Reasoning effort handed to the CLI backend (`--effort`: low, medium, high, xhigh, max).
|
|
75
|
+
*
|
|
76
|
+
* The criteria this tier rules on are the ones no engine can decide — alt relevance, link
|
|
77
|
+
* purpose in context, reading order — so how hard the model is asked to think is a real
|
|
78
|
+
* lever on the verdict, and one a caller should be able to move without also changing the
|
|
79
|
+
* model. Unlike `--max-turns`, `--effort` IS a flag of this CLI, so it is passed rather
|
|
80
|
+
* than refused. The Messages backend ignores it: effort is a session notion there, not a
|
|
81
|
+
* request parameter. */
|
|
82
|
+
effort?: string;
|
|
74
83
|
/** Called with each invocation's real cost, so a run can report what it spent instead of
|
|
75
84
|
* leaving the reader to find it in a provider dashboard. */
|
|
76
85
|
onCost?: (usd: number) => void;
|
package/src/standards/pack.ts
CHANGED
|
@@ -134,20 +134,61 @@ export interface SiblingCriterion {
|
|
|
134
134
|
export function siblingCriteria(pack: StandardPack, id: string, lang: string = pack.defaultLocale): SiblingCriterion[] {
|
|
135
135
|
const c = getCriterion(pack, id);
|
|
136
136
|
if (!c) return [];
|
|
137
|
-
|
|
137
|
+
return neighbourhood(pack, c)
|
|
138
|
+
.slice(0, MAX_SIBLINGS)
|
|
139
|
+
.map(({ crit }) => sibling(pack, crit, lang));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Mechanical: an engine rule can fail it. A criterion the pack declares `judgment`, and one
|
|
143
|
+
* that simply carries no rule, are both on the other side of the line. */
|
|
144
|
+
const isMechanical = (x: PackCriterion) => !x.judgment && (x.appliesTo?.ruleIds ?? []).length > 0;
|
|
145
|
+
|
|
146
|
+
const sibling = (pack: StandardPack, x: PackCriterion, lang: string): SiblingCriterion => ({
|
|
147
|
+
id: x.id,
|
|
148
|
+
// The plain title is the whole argument: « a-t-il une étiquette ? » beside « cette
|
|
149
|
+
// étiquette est-elle pertinente ? » explains the split better than any prose could.
|
|
150
|
+
title: localize(pack, x.titlePlain ?? x.title, lang),
|
|
151
|
+
role: isMechanical(x) ? "mechanical" : "judgment",
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
/** The full ranked neighbourhood, BEFORE the reading-aid cap — closest first, ties in
|
|
155
|
+
* criterion order. `siblingCriteria` shows the top of it; `presupposedCriterion` needs the
|
|
156
|
+
* whole thing, because a cap can hide the very tie that disqualifies a refusal. */
|
|
157
|
+
function neighbourhood(pack: StandardPack, c: PackCriterion): { crit: PackCriterion; shared: number }[] {
|
|
138
158
|
const mine = new Set(c.wcag ?? []);
|
|
139
|
-
const
|
|
159
|
+
const mech = isMechanical(c);
|
|
140
160
|
return pack.criteria
|
|
141
|
-
.
|
|
142
|
-
.
|
|
143
|
-
.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
161
|
+
.map((crit) => ({ crit, shared: (crit.wcag ?? []).filter((w) => mine.has(w)).length }))
|
|
162
|
+
.filter(({ crit, shared }) => crit.id !== c.id && crit.theme === c.theme && shared > 0 && isMechanical(crit) !== mech)
|
|
163
|
+
.sort((a, b) => b.shared - a.shared || a.crit.id.localeCompare(b.crit.id, undefined, { numeric: true }));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** THE NEIGHBOUR WHOSE SUBJECT THIS CRITERION PRESUPPOSES — or none, and none is the honest
|
|
167
|
+
* answer far more often than the neighbourhood is empty.
|
|
168
|
+
*
|
|
169
|
+
* `siblingCriteria` is a READING AID: « this observation may belong next door », three
|
|
170
|
+
* candidates, no claim that any one of them comes first. That breadth is right for a brief
|
|
171
|
+
* and wrong for a refusal, and the difference cost a true finding. RGAA 11.4 (« l'étiquette
|
|
172
|
+
* est-elle accolée à son champ ? ») neighbours 11.1, 11.5 and 11.6, each exactly one success
|
|
173
|
+
* criterion away. Only 11.1 owns 11.4's subject: with no label there is nothing to place.
|
|
174
|
+
* 11.5 (« les champs de même nature sont-ils regroupés ? ») owns none of it — a radio group
|
|
175
|
+
* that was never grouped still has labels, and they are still placed well or badly. Reading
|
|
176
|
+
* the neighbourhood as a presupposition made an ungrouped radio immune to 11.4.
|
|
177
|
+
*
|
|
178
|
+
* So: ONE neighbour, strictly closer than every other, mechanical (the side that can be
|
|
179
|
+
* failed without a model, which is what makes « the engine already owns this anchor » sayable
|
|
180
|
+
* at all). A tie is the pack declining to say which question comes first, and a refusal on a
|
|
181
|
+
* tie is a guess made against a finding that may well be true. On RGAA the rule keeps both
|
|
182
|
+
* pairs the split was drawn for — 11.2 → 11.1 (two shared success criteria against one) and
|
|
183
|
+
* 8.6 → 8.5 (sole neighbour) — and stands down on the thirteen criteria whose neighbours are
|
|
184
|
+
* equidistant. */
|
|
185
|
+
export function presupposedCriterion(pack: StandardPack, id: string, lang: string = pack.defaultLocale): SiblingCriterion | undefined {
|
|
186
|
+
const c = getCriterion(pack, id);
|
|
187
|
+
if (!c || isMechanical(c)) return undefined;
|
|
188
|
+
const ranked = neighbourhood(pack, c).filter(({ crit }) => isMechanical(crit));
|
|
189
|
+
const first = ranked[0];
|
|
190
|
+
if (!first || ranked[1]?.shared === first.shared) return undefined;
|
|
191
|
+
return sibling(pack, first.crit, lang);
|
|
151
192
|
}
|
|
152
193
|
|
|
153
194
|
/** Where the standard publishes this criterion, from the pack's `criterionUrl` template.
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Single source of truth for shared types + the embedded version. sync-version.mjs
|
|
2
2
|
// rewrites VERSION at release time (kept in lockstep with package.json + SKILL.md).
|
|
3
|
-
export const VERSION = "5.
|
|
3
|
+
export const VERSION = "5.34.0";
|
|
4
4
|
export const SCHEMA_VERSION = 2;
|
|
5
5
|
|
|
6
6
|
// Lang = the UI FRAME's language — the `L` localization tables each module (report,
|