@wcag-checkr/ci 1.0.0-rc.31 → 1.0.0-rc.310
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/ErrorBoundary-C-kswn4E.js +594 -0
- package/dist/assets/ai-usage-log-BX3L6bKl.js +1 -0
- package/dist/assets/content-script.ts-FuMy_sE5.js +217 -0
- package/dist/assets/{content-script.ts-loader-Dfu1UEfD.js → content-script.ts-loader-CBHeu186.js} +1 -1
- package/dist/assets/copy-ai-fixer-prompt-DQYkHOv3.js +19 -0
- package/dist/assets/{crash-reporter-Dc5lvxvY.js → crash-reporter-Bu2p8K-p.js} +1 -1
- package/dist/assets/design-system-audit-DpxJrxnb.js +1 -0
- package/dist/assets/devtools-panel-DFQvqKKj.js +1 -0
- package/dist/assets/diff-DA41zYPc.js +1 -0
- package/dist/assets/dom-criterion-analyzers-DoUaJV5C.js +8 -0
- package/dist/assets/fraunces-latin-400-normal-6IfK1voy.woff2 +0 -0
- package/dist/assets/fraunces-latin-400-normal-NUPT2cO8.woff +0 -0
- package/dist/assets/fraunces-latin-500-normal-BTR4KCeb.woff +0 -0
- package/dist/assets/fraunces-latin-500-normal-DnGCNyPD.woff2 +0 -0
- package/dist/assets/fraunces-latin-600-normal-BFCDtZfi.woff2 +0 -0
- package/dist/assets/fraunces-latin-600-normal-DL5QCzvS.woff +0 -0
- package/dist/assets/fraunces-latin-ext-400-normal-D8gbi3Gu.woff2 +0 -0
- package/dist/assets/fraunces-latin-ext-400-normal-UihxqfOe.woff +0 -0
- package/dist/assets/fraunces-latin-ext-500-normal-BMcFk1Xs.woff +0 -0
- package/dist/assets/fraunces-latin-ext-500-normal-Z5DV8IzT.woff2 +0 -0
- package/dist/assets/fraunces-latin-ext-600-normal-B0Dy4lqi.woff +0 -0
- package/dist/assets/fraunces-latin-ext-600-normal-BtzmzP0X.woff2 +0 -0
- package/dist/assets/fraunces-vietnamese-400-normal-B65MOf9T.woff +0 -0
- package/dist/assets/fraunces-vietnamese-400-normal-CvGt0Ybw.woff2 +0 -0
- package/dist/assets/fraunces-vietnamese-500-normal-B-KbxExq.woff +0 -0
- package/dist/assets/fraunces-vietnamese-500-normal-GOH_-EGq.woff2 +0 -0
- package/dist/assets/fraunces-vietnamese-600-normal-BjlAJixd.woff2 +0 -0
- package/dist/assets/fraunces-vietnamese-600-normal-DlAl5EAR.woff +0 -0
- package/dist/assets/geist-sans-latin-400-normal-BOaIZNA2.woff +0 -0
- package/dist/assets/geist-sans-latin-400-normal-gapTbOY8.woff2 +0 -0
- package/dist/assets/geist-sans-latin-500-normal-CN2lyvyL.woff +0 -0
- package/dist/assets/geist-sans-latin-500-normal-uokXdC-Q.woff2 +0 -0
- package/dist/assets/geist-sans-latin-600-normal-CA1yjETN.woff +0 -0
- package/dist/assets/geist-sans-latin-600-normal-DFOURf8L.woff2 +0 -0
- package/dist/assets/geist-sans-latin-700-normal-BmN9tIp5.woff2 +0 -0
- package/dist/assets/geist-sans-latin-700-normal-CjScfYeH.woff +0 -0
- package/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/dist/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/dist/assets/jetbrains-mono-cyrillic-600-normal-8K4wrrwR.woff +0 -0
- package/dist/assets/jetbrains-mono-cyrillic-600-normal-EVf6-Yzo.woff2 +0 -0
- package/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/dist/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/dist/assets/jetbrains-mono-greek-600-normal-H7WoG9Et.woff2 +0 -0
- package/dist/assets/jetbrains-mono-greek-600-normal-mc2nkWzM.woff +0 -0
- package/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/dist/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
- package/dist/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/dist/assets/jetbrains-mono-latin-ext-600-normal-BfB_LPfz.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-ext-600-normal-DObL3zCW.woff +0 -0
- package/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/dist/assets/jetbrains-mono-vietnamese-600-normal-OWROknRo.woff +0 -0
- package/dist/assets/options-BPhjrbGI.js +6 -0
- package/dist/assets/parallel-tab-flow-Xk9RSjay.js +1 -0
- package/dist/assets/scheduled-audit-runner-DyKpb3zg.js +2167 -0
- package/dist/assets/service-worker.ts-CMkltOzu.js +2 -0
- package/dist/assets/side-panel-Ctm2yXeo.css +1 -0
- package/dist/assets/side-panel-f_X4NOJt.js +4 -0
- package/dist/assets/site-report-renderer-DNgytqhZ.js +189 -0
- package/dist/assets/{styles-C4Kq0zOO.js → styles-Cn731SYD.js} +13 -13
- package/dist/assets/styles-d5msFsnl.css +1 -0
- package/dist/assets/zip-encoder-CtULHXx_.js +1 -0
- package/dist/axe.min.js +12 -0
- package/dist/devtools/panel.html +9 -8
- package/dist/manifest.json +11 -8
- package/dist/options/options.html +5 -6
- package/dist/service-worker-loader.js +1 -1
- package/dist/side-panel/App.tsx +129 -5
- package/dist/side-panel/audit-launcher.ts +21 -1
- package/dist/side-panel/azure-devops-issue.test.ts +68 -0
- package/dist/side-panel/azure-devops-issue.ts +89 -0
- package/dist/side-panel/gitlab-issue.test.ts +53 -0
- package/dist/side-panel/gitlab-issue.ts +78 -0
- package/dist/side-panel/main.tsx +39 -2
- package/dist/side-panel/side-panel.html +11 -8
- package/dist/side-panel/store.ts +149 -13
- package/dist/side-panel/styles.css +39 -0
- package/dist/side-panel/wire-messaging.ts +146 -9
- package/package.json +1 -1
- package/wcagcheckr-ci.mjs +193 -32
- package/dist/assets/ErrorBoundary-BLcMSVSr.js +0 -524
- package/dist/assets/ai-usage-log-Dj9Ub_DT.js +0 -1
- package/dist/assets/content-script.ts-CwcUMq3e.js +0 -181
- package/dist/assets/devtools-panel-DQ3Bbomf.js +0 -1
- package/dist/assets/diff-D4sCAdXf.js +0 -1
- package/dist/assets/forensic-log-B1UCXZ23.js +0 -129
- package/dist/assets/options-BG2i5vFf.js +0 -6
- package/dist/assets/preload-helper-D7HrI6pR.js +0 -1
- package/dist/assets/service-worker.ts-CO86CV_p.js +0 -715
- package/dist/assets/side-panel-XSB07vDa.js +0 -1
- package/dist/assets/site-report-renderer-CyHkM6hB.js +0 -147
- package/dist/assets/state-PELIq3oj.js +0 -1
- package/dist/assets/styles-Cevp58mS.css +0 -1
package/dist/side-panel/main.tsx
CHANGED
|
@@ -1,19 +1,56 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import { createRoot } from 'react-dom/client';
|
|
3
3
|
import { App } from './App';
|
|
4
|
+
import { V2App } from './v2/App';
|
|
4
5
|
import { ErrorBoundary } from './components/ErrorBoundary';
|
|
5
6
|
import { installCrashReporter } from '../modules/crash-reporter';
|
|
6
7
|
import './styles.css';
|
|
7
8
|
|
|
8
9
|
installCrashReporter('side-panel');
|
|
9
10
|
|
|
11
|
+
const V2_STORAGE_KEY = 'v2UiEnabled';
|
|
12
|
+
|
|
13
|
+
/** rc.276 — Wrapper that picks the v1 or v2 UI based on a stored
|
|
14
|
+
* preference. Lives one layer above either App so a user can
|
|
15
|
+
* toggle without reloading the extension; toggling re-mounts. */
|
|
16
|
+
function Root() {
|
|
17
|
+
const [enabled, setEnabled] = useState<boolean | null>(null);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
let cancelled = false;
|
|
20
|
+
void chrome.storage.local
|
|
21
|
+
.get(V2_STORAGE_KEY)
|
|
22
|
+
.then((r) => {
|
|
23
|
+
if (cancelled) return;
|
|
24
|
+
setEnabled(Boolean(r[V2_STORAGE_KEY]));
|
|
25
|
+
})
|
|
26
|
+
.catch(() => setEnabled(false));
|
|
27
|
+
// Listen for live changes so toggling from Settings re-renders.
|
|
28
|
+
const onChange = (
|
|
29
|
+
changes: Record<string, chrome.storage.StorageChange>,
|
|
30
|
+
area: string,
|
|
31
|
+
) => {
|
|
32
|
+
if (area !== 'local') return;
|
|
33
|
+
if (V2_STORAGE_KEY in changes) {
|
|
34
|
+
setEnabled(Boolean(changes[V2_STORAGE_KEY]?.newValue));
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
chrome.storage.onChanged.addListener(onChange);
|
|
38
|
+
return () => {
|
|
39
|
+
cancelled = true;
|
|
40
|
+
chrome.storage.onChanged.removeListener(onChange);
|
|
41
|
+
};
|
|
42
|
+
}, []);
|
|
43
|
+
if (enabled === null) return null; // brief tick before first paint
|
|
44
|
+
return enabled ? <V2App /> : <App />;
|
|
45
|
+
}
|
|
46
|
+
|
|
10
47
|
const root = document.getElementById('root');
|
|
11
48
|
if (!root) throw new Error('side-panel: #root not found');
|
|
12
49
|
|
|
13
50
|
createRoot(root).render(
|
|
14
51
|
<React.StrictMode>
|
|
15
52
|
<ErrorBoundary>
|
|
16
|
-
<
|
|
53
|
+
<Root />
|
|
17
54
|
</ErrorBoundary>
|
|
18
55
|
</React.StrictMode>
|
|
19
56
|
);
|
|
@@ -4,16 +4,19 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>WCAG Component Auditor</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/side-panel-
|
|
7
|
+
<script type="module" crossorigin src="/assets/side-panel-f_X4NOJt.js"></script>
|
|
8
8
|
<link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/_commonjsHelpers-Cpj98o6Y.js">
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/crash-reporter-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
12
|
-
<link rel="modulepreload" crossorigin href="/assets/styles-
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
14
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
15
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
16
|
-
<link rel="
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/crash-reporter-Bu2p8K-p.js">
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/ai-usage-log-BX3L6bKl.js">
|
|
12
|
+
<link rel="modulepreload" crossorigin href="/assets/styles-Cn731SYD.js">
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/diff-DA41zYPc.js">
|
|
14
|
+
<link rel="modulepreload" crossorigin href="/assets/scheduled-audit-runner-DyKpb3zg.js">
|
|
15
|
+
<link rel="modulepreload" crossorigin href="/assets/design-system-audit-DpxJrxnb.js">
|
|
16
|
+
<link rel="modulepreload" crossorigin href="/assets/ErrorBoundary-C-kswn4E.js">
|
|
17
|
+
<link rel="modulepreload" crossorigin href="/assets/copy-ai-fixer-prompt-DQYkHOv3.js">
|
|
18
|
+
<link rel="stylesheet" crossorigin href="/assets/styles-d5msFsnl.css">
|
|
19
|
+
<link rel="stylesheet" crossorigin href="/assets/side-panel-Ctm2yXeo.css">
|
|
17
20
|
</head>
|
|
18
21
|
<body class="m-0 p-0">
|
|
19
22
|
<div id="root"></div>
|
package/dist/side-panel/store.ts
CHANGED
|
@@ -7,9 +7,15 @@ import type { StateConfig } from '../types/state';
|
|
|
7
7
|
import type { LicenseTier } from '../shared/messages';
|
|
8
8
|
import type { InAppMessage } from '../modules/messages-client';
|
|
9
9
|
import type { SiteCrawlReport } from '../shared/site-aggregator';
|
|
10
|
+
import { saveSiteCrawlReport } from '../shared/site-crawl-storage';
|
|
11
|
+
import type { IncompleteResolution } from '../shared/wcag-verdicts';
|
|
10
12
|
|
|
11
13
|
export type AuditStatus = 'idle' | 'running' | 'complete' | 'failed' | 'interrupted';
|
|
12
|
-
export type View = 'matrix' | 'delta' | 'activity' | 'guided' | 'flows' | 'scorecard' | 'crawl' | 'forensic' | 'compliance';
|
|
14
|
+
export type View = 'matrix' | 'report' | 'delta' | 'activity' | 'guided' | 'flows' | 'scorecard' | 'crawl' | 'forensic' | 'compliance' | 'schedules' | 'risk' | 'copilot' | 'ax-tree' | 'wcag3' | 'tokens';
|
|
15
|
+
// rc.159 — Findings sub-nav. The "Findings" (report) view is now a hub
|
|
16
|
+
// containing six lenses on the same audit data. The user picks which
|
|
17
|
+
// lens; the slug stays 'report' so existing tests/probes keep working.
|
|
18
|
+
export type FindingsLens = 'overview' | 'per-area' | 'violations' | 'activity' | 'delta' | 'wcag3';
|
|
13
19
|
/** Two distinct personas: site owners (mom-and-pop, no code) vs developers
|
|
14
20
|
* (full feature surface, CLI-friendly outputs, baselines, exports). */
|
|
15
21
|
export type UserMode = 'owner' | 'dev';
|
|
@@ -64,6 +70,9 @@ type Store = {
|
|
|
64
70
|
|
|
65
71
|
// ui
|
|
66
72
|
view: View;
|
|
73
|
+
/** rc.159 — Which lens to show inside the Findings view. Defaults to
|
|
74
|
+
* 'overview' on every audit completion. */
|
|
75
|
+
findingsLens: FindingsLens;
|
|
67
76
|
/** Persona-driven UI mode. `null` = first-launch (wizard not yet answered). */
|
|
68
77
|
userMode: UserMode | null;
|
|
69
78
|
|
|
@@ -78,6 +87,15 @@ type Store = {
|
|
|
78
87
|
pinnedMatchKey: string | null;
|
|
79
88
|
pinnedFound: boolean;
|
|
80
89
|
|
|
90
|
+
/** rc.222 — Wall-clock audit timing. `startNewScan` writes Date.now()
|
|
91
|
+
* when the user kicks off an audit; `setResults` computes the delta
|
|
92
|
+
* on completion. Pre-rc.222 the GradeCard added per-state durations
|
|
93
|
+
* but missed the AI walkthroughs + pixel-contrast sampler that run
|
|
94
|
+
* AFTER the matrix — making the displayed "21.4s" significantly less
|
|
95
|
+
* than the user's real wait. Now we capture true elapsed time. */
|
|
96
|
+
auditStartedAtMs: number | null;
|
|
97
|
+
lastAuditWallClockMs: number | null;
|
|
98
|
+
|
|
81
99
|
// AI augmentation failure surface — shown as a dismissible banner above the
|
|
82
100
|
// results area when an audit's AI portion errored despite a configured key.
|
|
83
101
|
// Cleared whenever a new audit starts or completes cleanly.
|
|
@@ -98,6 +116,12 @@ type Store = {
|
|
|
98
116
|
currentCheckLabel: string;
|
|
99
117
|
current: number;
|
|
100
118
|
total: number;
|
|
119
|
+
/** Optional sub-progress within the current check: which candidate is
|
|
120
|
+
* being judged (e.g. image 3 of 10). When present the UI shows
|
|
121
|
+
* "AI: 7/9 — Reviewing link clarity… (3/10)" so the user can see
|
|
122
|
+
* movement inside a single check that judges many candidates. */
|
|
123
|
+
candidatesDone?: number;
|
|
124
|
+
candidatesTotal?: number;
|
|
101
125
|
} | null;
|
|
102
126
|
|
|
103
127
|
// actions
|
|
@@ -129,15 +153,76 @@ type Store = {
|
|
|
129
153
|
}
|
|
130
154
|
) => void;
|
|
131
155
|
setView: (v: View) => void;
|
|
156
|
+
setFindingsLens: (lens: FindingsLens) => void;
|
|
157
|
+
/** rc.205 — When the user clicks a Verification Area tile (or a
|
|
158
|
+
* criterion ID on the WCAG AA Status banner) we want the Per-area
|
|
159
|
+
* lens to render with THAT area's card already expanded. PerAreaCards
|
|
160
|
+
* reads this on mount, expands the matching area, then clears the
|
|
161
|
+
* request via `clearPendingAreaExpand`. Null when no jump is pending. */
|
|
162
|
+
pendingAreaExpand: string | null;
|
|
163
|
+
setPendingAreaExpand: (id: string | null) => void;
|
|
132
164
|
setUserMode: (m: UserMode | null) => void;
|
|
133
165
|
setSiteCrawlStatus: (s: Store['siteCrawlStatus']) => void;
|
|
134
166
|
setSiteCrawlProgress: (p: Store['siteCrawlProgress']) => void;
|
|
135
167
|
setSiteCrawlReport: (r: SiteCrawlReport | null) => void;
|
|
136
168
|
setSiteCrawlError: (e: string | null) => void;
|
|
169
|
+
/** rc.234 — When the Findings tab is showing a slice of crawl data
|
|
170
|
+
* (either the site-wide aggregate or a specific page from the
|
|
171
|
+
* crawl), this carries the navigation context so the user can
|
|
172
|
+
* flip between them without losing the crawl. Null means "no
|
|
173
|
+
* active crawl context" (regular single-page audit).
|
|
174
|
+
* `mode: 'site-aggregate'` — Findings shows merged crawl data
|
|
175
|
+
* `mode: 'page', url: 'X'` — Findings shows one crawled page
|
|
176
|
+
* The ScopeBanner uses this to surface a page selector +
|
|
177
|
+
* "back to site-wide" affordance. The "Pages, worst first" list
|
|
178
|
+
* on the Crawl panel and the "← Back to site-wide" button on
|
|
179
|
+
* Findings both flip this state. */
|
|
180
|
+
crawlNavContext: { mode: 'site-aggregate' } | { mode: 'page'; url: string } | null;
|
|
181
|
+
setCrawlNavContext: (ctx: Store['crawlNavContext']) => void;
|
|
182
|
+
/** rc.235 — Synchronous cache of incomplete-resolution AI verdicts
|
|
183
|
+
* keyed by pageUrl. Pre-rc.235 the `useCurrentlyRelevantResolutions`
|
|
184
|
+
* hook always started with `[]` and async-loaded from chrome.storage,
|
|
185
|
+
* producing a TWO-RENDER FLICKER: first render counts violations
|
|
186
|
+
* WITHOUT AI fails, second render after async load includes them.
|
|
187
|
+
* Most visible when drilling into a crawled page (cached AI verdicts
|
|
188
|
+
* exist immediately), where the header would show "Fix 1 element"
|
|
189
|
+
* then jump to "Fix 5 elements" within milliseconds — Cliff: "things
|
|
190
|
+
* already do not make sense."
|
|
191
|
+
*
|
|
192
|
+
* The cache is in-memory only (no chrome.storage write). Callers
|
|
193
|
+
* pre-warm via `setResolutionsForUrl(url, resolutions)` BEFORE
|
|
194
|
+
* calling `setResults` so the hook's lazy initial value reads
|
|
195
|
+
* from cache instead of `[]`. The hook still runs its own async
|
|
196
|
+
* refresh to confirm + pick up any updates. */
|
|
197
|
+
resolutionsByUrl: Record<string, IncompleteResolution[]>;
|
|
198
|
+
setResolutionsForUrl: (url: string, resolutions: IncompleteResolution[]) => void;
|
|
137
199
|
setPinned: (matchKey: string | null, found?: boolean) => void;
|
|
138
200
|
setAiFailure: (failure: Store['aiFailure']) => void;
|
|
139
201
|
clearAiFailure: () => void;
|
|
140
202
|
setAiProgress: (progress: Store['aiProgress']) => void;
|
|
203
|
+
/** rc.37 — matchKeys the user has acknowledged as "not an issue." Loaded
|
|
204
|
+
* from chrome.storage at mount. Every ViolationCard reads this directly
|
|
205
|
+
* so the "Acknowledged" pill appears immediately on click, in every
|
|
206
|
+
* view (Matrix, Delta, WCAG-em, Owner — anywhere a card renders),
|
|
207
|
+
* before the SW round-trip to refresh the delta. */
|
|
208
|
+
acknowledgedKeys: Set<string>;
|
|
209
|
+
setAcknowledgedKeys: (keys: Set<string>) => void;
|
|
210
|
+
markAcknowledged: (matchKey: string) => void;
|
|
211
|
+
markUnacknowledged: (matchKey: string) => void;
|
|
212
|
+
/** rc.200 — Dismissal keys for axe violations (and AI-resolved fails).
|
|
213
|
+
* Distinct from `acknowledgedKeys`:
|
|
214
|
+
* - Acknowledged = "I verified the content; the rule was right
|
|
215
|
+
* that this looked worth reviewing, but it's actually fine."
|
|
216
|
+
* matchKey-based; survives re-audits as long as content matches.
|
|
217
|
+
* - Dismissed = "The rule itself is wrong about this page."
|
|
218
|
+
* URL-scoped; filtered from future audits + the AI-fix prompt.
|
|
219
|
+
* Loaded from `shared/dismissals` (chrome.storage.local) on audit-
|
|
220
|
+
* page change. ViolationCard renders the corresponding pill +
|
|
221
|
+
* un-dismiss affordance based on this set. */
|
|
222
|
+
dismissedKeys: Set<string>;
|
|
223
|
+
setDismissedKeys: (keys: Set<string>) => void;
|
|
224
|
+
markDismissed: (dismissalKey: string) => void;
|
|
225
|
+
markUndismissed: (dismissalKey: string) => void;
|
|
141
226
|
/** Replace the in-app messages list + counters. Called by wire-messaging
|
|
142
227
|
* after a refresh from the server. */
|
|
143
228
|
setMessages: (messages: InAppMessage[], unreadCount: number, criticalUnacked: boolean) => void;
|
|
@@ -166,15 +251,22 @@ export const useStore = create<Store>((set) => ({
|
|
|
166
251
|
unreadMessageCount: 0,
|
|
167
252
|
criticalUnacked: false,
|
|
168
253
|
view: 'matrix',
|
|
254
|
+
findingsLens: 'overview',
|
|
169
255
|
userMode: null,
|
|
170
256
|
siteCrawlStatus: 'idle',
|
|
171
257
|
siteCrawlProgress: null,
|
|
172
258
|
siteCrawlReport: null,
|
|
173
259
|
siteCrawlError: null,
|
|
260
|
+
crawlNavContext: null,
|
|
261
|
+
resolutionsByUrl: {},
|
|
174
262
|
pinnedMatchKey: null,
|
|
175
263
|
pinnedFound: true,
|
|
176
264
|
aiFailure: null,
|
|
177
265
|
aiProgress: null,
|
|
266
|
+
acknowledgedKeys: new Set<string>(),
|
|
267
|
+
pendingAreaExpand: null,
|
|
268
|
+
auditStartedAtMs: null,
|
|
269
|
+
lastAuditWallClockMs: null,
|
|
178
270
|
|
|
179
271
|
setStatus: (status) => set({ status }),
|
|
180
272
|
startNewScan: () =>
|
|
@@ -189,21 +281,32 @@ export const useStore = create<Store>((set) => ({
|
|
|
189
281
|
pinnedFound: true,
|
|
190
282
|
aiFailure: null,
|
|
191
283
|
aiProgress: null,
|
|
284
|
+
auditStartedAtMs: Date.now(),
|
|
285
|
+
// rc.234 — Single-page re-audit clears any crawl drill-in
|
|
286
|
+
// context so the user doesn't see a stale "Back to site-wide"
|
|
287
|
+
// affordance pointing at a no-longer-applicable crawl.
|
|
288
|
+
crawlNavContext: null,
|
|
192
289
|
// freshThisSession stays false until setResults fires.
|
|
193
290
|
}),
|
|
194
291
|
setProgress: (progress) => set({ progress }),
|
|
195
292
|
setResults: (results, delta, componentId) =>
|
|
196
|
-
set({
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
293
|
+
set((state) => {
|
|
294
|
+
const wallClockMs = state.auditStartedAtMs
|
|
295
|
+
? Date.now() - state.auditStartedAtMs
|
|
296
|
+
: null;
|
|
297
|
+
return {
|
|
298
|
+
results,
|
|
299
|
+
delta,
|
|
300
|
+
componentId,
|
|
301
|
+
status: 'complete',
|
|
302
|
+
errorMessage: null,
|
|
303
|
+
freshThisSession: true,
|
|
304
|
+
aiProgress: null,
|
|
305
|
+
pinnedMatchKey: null,
|
|
306
|
+
pinnedFound: true,
|
|
307
|
+
lastAuditWallClockMs: wallClockMs,
|
|
308
|
+
auditStartedAtMs: null,
|
|
309
|
+
};
|
|
207
310
|
}),
|
|
208
311
|
setDelta: (delta) => set({ delta }),
|
|
209
312
|
setError: (errorMessage) => set({ errorMessage, status: 'failed' }),
|
|
@@ -233,15 +336,48 @@ export const useStore = create<Store>((set) => ({
|
|
|
233
336
|
pastDue: extras?.pastDue ?? false,
|
|
234
337
|
}),
|
|
235
338
|
setView: (view) => set({ view }),
|
|
339
|
+
setFindingsLens: (findingsLens) => set({ findingsLens }),
|
|
340
|
+
setPendingAreaExpand: (pendingAreaExpand) => set({ pendingAreaExpand }),
|
|
236
341
|
setUserMode: (userMode) => set({ userMode }),
|
|
237
342
|
setSiteCrawlStatus: (siteCrawlStatus) => set({ siteCrawlStatus }),
|
|
343
|
+
setCrawlNavContext: (crawlNavContext) => set({ crawlNavContext }),
|
|
344
|
+
setResolutionsForUrl: (url, resolutions) =>
|
|
345
|
+
set((state) => ({
|
|
346
|
+
resolutionsByUrl: { ...state.resolutionsByUrl, [url]: resolutions },
|
|
347
|
+
})),
|
|
238
348
|
setSiteCrawlProgress: (siteCrawlProgress) => set({ siteCrawlProgress }),
|
|
239
|
-
setSiteCrawlReport: (siteCrawlReport) =>
|
|
349
|
+
setSiteCrawlReport: (siteCrawlReport) => {
|
|
350
|
+
set({ siteCrawlReport });
|
|
351
|
+
// rc.111 — persist so the AI fixer prompt's site-wide context section
|
|
352
|
+
// survives panel close / SW restart. Fire-and-forget; failure to write
|
|
353
|
+
// chrome.storage is non-fatal (the in-memory state still works for the
|
|
354
|
+
// current session).
|
|
355
|
+
void saveSiteCrawlReport(siteCrawlReport);
|
|
356
|
+
},
|
|
240
357
|
setSiteCrawlError: (siteCrawlError) => set({ siteCrawlError }),
|
|
241
358
|
setPinned: (matchKey, found = true) => set({ pinnedMatchKey: matchKey, pinnedFound: found }),
|
|
242
359
|
setAiFailure: (aiFailure) => set({ aiFailure }),
|
|
243
360
|
clearAiFailure: () => set({ aiFailure: null }),
|
|
244
361
|
setAiProgress: (aiProgress) => set({ aiProgress }),
|
|
362
|
+
setAcknowledgedKeys: (acknowledgedKeys) => set({ acknowledgedKeys }),
|
|
363
|
+
markAcknowledged: (matchKey) =>
|
|
364
|
+
set((s) => ({ acknowledgedKeys: new Set(s.acknowledgedKeys).add(matchKey) })),
|
|
365
|
+
markUnacknowledged: (matchKey) =>
|
|
366
|
+
set((s) => {
|
|
367
|
+
const next = new Set(s.acknowledgedKeys);
|
|
368
|
+
next.delete(matchKey);
|
|
369
|
+
return { acknowledgedKeys: next };
|
|
370
|
+
}),
|
|
371
|
+
dismissedKeys: new Set<string>(),
|
|
372
|
+
setDismissedKeys: (dismissedKeys) => set({ dismissedKeys }),
|
|
373
|
+
markDismissed: (dismissalKey) =>
|
|
374
|
+
set((s) => ({ dismissedKeys: new Set(s.dismissedKeys).add(dismissalKey) })),
|
|
375
|
+
markUndismissed: (dismissalKey) =>
|
|
376
|
+
set((s) => {
|
|
377
|
+
const next = new Set(s.dismissedKeys);
|
|
378
|
+
next.delete(dismissalKey);
|
|
379
|
+
return { dismissedKeys: next };
|
|
380
|
+
}),
|
|
245
381
|
setMessages: (messages, unreadMessageCount, criticalUnacked) =>
|
|
246
382
|
set({ messages, unreadMessageCount, criticalUnacked }),
|
|
247
383
|
applyMessageAck: (messageId, action) =>
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/* rc.159 — Forensic-clarity type system. IBM Plex Sans for body + display
|
|
2
|
+
(humanist sans with strong character for a compliance-tool aesthetic);
|
|
3
|
+
IBM Plex Mono for selectors, matchKeys, JSON output, axe rule IDs.
|
|
4
|
+
Bundled via Google Fonts; Chrome caches woff2 across pages so subsequent
|
|
5
|
+
side-panel opens are offline-friendly. */
|
|
6
|
+
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
|
|
7
|
+
|
|
1
8
|
@tailwind base;
|
|
2
9
|
@tailwind components;
|
|
3
10
|
@tailwind utilities;
|
|
@@ -6,6 +13,38 @@ html, body, #root {
|
|
|
6
13
|
height: 100%;
|
|
7
14
|
}
|
|
8
15
|
|
|
16
|
+
html {
|
|
17
|
+
/* Default to the Plex stack; mono utilities switch to the mono stack. */
|
|
18
|
+
font-family:
|
|
19
|
+
'IBM Plex Sans',
|
|
20
|
+
-apple-system,
|
|
21
|
+
BlinkMacSystemFont,
|
|
22
|
+
'Segoe UI Variable Text',
|
|
23
|
+
'Segoe UI',
|
|
24
|
+
system-ui,
|
|
25
|
+
sans-serif;
|
|
26
|
+
/* Slightly tighter feature defaults to match the compliance-doc feel:
|
|
27
|
+
stylistic alternates + tabular numerals so columns of counts and
|
|
28
|
+
percentages align visually inside tables and per-area cards. */
|
|
29
|
+
font-feature-settings: 'tnum' 1, 'ss01' 1, 'cv11' 1;
|
|
30
|
+
-webkit-font-smoothing: antialiased;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
code,
|
|
34
|
+
kbd,
|
|
35
|
+
samp,
|
|
36
|
+
pre,
|
|
37
|
+
.font-mono {
|
|
38
|
+
font-family:
|
|
39
|
+
'IBM Plex Mono',
|
|
40
|
+
ui-monospace,
|
|
41
|
+
'JetBrains Mono',
|
|
42
|
+
'SF Mono',
|
|
43
|
+
'Consolas',
|
|
44
|
+
monospace;
|
|
45
|
+
font-feature-settings: 'tnum' 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
9
48
|
/* Tailwind's preflight resets the UA focus ring. Restore a high-contrast
|
|
10
49
|
ring for keyboard users (only :focus-visible — pointer focus stays clean).
|
|
11
50
|
Brand-700 #3730a3 over white = 9.6:1 contrast; same ring shows fine on
|
|
@@ -5,9 +5,16 @@ import { on, request } from '../shared/messaging';
|
|
|
5
5
|
import { useStore } from './store';
|
|
6
6
|
import { announcer } from '../shared/announcer';
|
|
7
7
|
import { getAuditTargetTabId } from '../shared/active-tab';
|
|
8
|
-
import type { BaselineListResponse, SettingsResponse, TierGetResponse } from '../shared/messages';
|
|
8
|
+
import type { BaselineListResponse, SettingsResponse, TierGetResponse, ExportResponse } from '../shared/messages';
|
|
9
9
|
import type { UserMode } from './store';
|
|
10
10
|
import type { AuditResult, DeltaResult } from '../types/audit';
|
|
11
|
+
import {
|
|
12
|
+
getAutoExportSettings,
|
|
13
|
+
recordLastAutoExport,
|
|
14
|
+
} from '../shared/auto-export-settings';
|
|
15
|
+
import { triggerDownload, bundleFilename } from '../shared/download-helper';
|
|
16
|
+
import { isFeatureAllowed } from '../shared/tier-rules';
|
|
17
|
+
import { isLockoutInEffectForTier } from '../shared/headless-build-lockdown';
|
|
11
18
|
|
|
12
19
|
const LAST_AUDIT_KEY = 'sidePanel:lastAudit';
|
|
13
20
|
|
|
@@ -22,17 +29,27 @@ export function wireMessaging(): () => void {
|
|
|
22
29
|
|
|
23
30
|
offs.push(
|
|
24
31
|
on('AUDIT_PROGRESS_EVENT', (msg) => {
|
|
25
|
-
const
|
|
26
|
-
|
|
32
|
+
const s = useStore.getState();
|
|
33
|
+
const wasRunning = s.status === 'running';
|
|
34
|
+
const inCrawl = s.siteCrawlStatus === 'running';
|
|
35
|
+
s.setProgress({
|
|
27
36
|
current: msg.current,
|
|
28
37
|
total: msg.total,
|
|
29
38
|
currentState: msg.currentState,
|
|
30
39
|
});
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
// rc.288 — During a crawl, AUDIT_PROGRESS_EVENT fires per page
|
|
41
|
+
// per state so the side panel can show inner matrix progress
|
|
42
|
+
// underneath the crawl's page counter. Don't flip s.status to
|
|
43
|
+
// 'running' in that case — the crawl's own siteCrawlStatus is
|
|
44
|
+
// the lifecycle signal, and we'd leave status='running' dangling
|
|
45
|
+
// after the crawl ends (AUDIT_COMPLETE is suppressed by
|
|
46
|
+
// asSubroutine). For genuine single-page runs, status='running'
|
|
47
|
+
// is still the right signal.
|
|
48
|
+
if (!inCrawl) {
|
|
49
|
+
s.setStatus('running');
|
|
50
|
+
if (!wasRunning) {
|
|
51
|
+
announcer.polite(`Audit running, scanning ${msg.total} state${msg.total === 1 ? '' : 's'}.`);
|
|
52
|
+
}
|
|
36
53
|
}
|
|
37
54
|
})
|
|
38
55
|
);
|
|
@@ -46,11 +63,30 @@ export function wireMessaging(): () => void {
|
|
|
46
63
|
? `Audit complete. ${newCount} new violation${newCount === 1 ? '' : 's'} versus baseline.`
|
|
47
64
|
: `Audit complete. ${violationCount} violation${violationCount === 1 ? '' : 's'} found across ${msg.results.length} state${msg.results.length === 1 ? '' : 's'}.`;
|
|
48
65
|
announcer.polite(summary);
|
|
66
|
+
// rc.159 — Auto-navigate to Findings on audit completion. The user
|
|
67
|
+
// most likely wants to see the results, not stay on the Dashboard
|
|
68
|
+
// / specialized tool they kicked the audit off from. Exception: if
|
|
69
|
+
// they're mid-flow on Compliance (multi-step report builder),
|
|
70
|
+
// Guided (interactive workflow session), or Crawl (multi-page scan
|
|
71
|
+
// in progress), don't yank them out of context.
|
|
72
|
+
const currentView = useStore.getState().view;
|
|
73
|
+
const stickyViews: Array<typeof currentView> = ['compliance', 'guided', 'crawl'];
|
|
74
|
+
if (!stickyViews.includes(currentView)) {
|
|
75
|
+
useStore.getState().setView('report');
|
|
76
|
+
useStore.getState().setFindingsLens('overview');
|
|
77
|
+
}
|
|
49
78
|
void persistLastAudit({
|
|
50
79
|
results: msg.results,
|
|
51
80
|
delta: msg.delta,
|
|
52
81
|
componentId: msg.componentId,
|
|
53
82
|
});
|
|
83
|
+
// rc.201 — Auto-export the audit report if the user has the
|
|
84
|
+
// "auto-export after each audit" setting on AND their tier
|
|
85
|
+
// allows it. Fires AFTER setResults so the rest of the UI has
|
|
86
|
+
// updated. Errors are best-effort + silent — the user already
|
|
87
|
+
// has the in-app result; failing to write a file is logged but
|
|
88
|
+
// doesn't disrupt the audit flow.
|
|
89
|
+
void maybeAutoExport(msg.results, msg.delta, msg.componentId);
|
|
54
90
|
})
|
|
55
91
|
);
|
|
56
92
|
|
|
@@ -70,9 +106,12 @@ export function wireMessaging(): () => void {
|
|
|
70
106
|
currentCheckLabel: msg.currentCheckLabel,
|
|
71
107
|
current: msg.current,
|
|
72
108
|
total: msg.total,
|
|
109
|
+
candidatesDone: msg.candidatesDone,
|
|
110
|
+
candidatesTotal: msg.candidatesTotal,
|
|
73
111
|
});
|
|
74
112
|
// Announce only on the first AI check so SR users hear the phase
|
|
75
|
-
// transition once, not 8 times.
|
|
113
|
+
// transition once, not 8 times. Sub-progress events fire often;
|
|
114
|
+
// don't speak those.
|
|
76
115
|
if (!prev) {
|
|
77
116
|
announcer.polite(`AI augmentation running ${msg.total} check${msg.total === 1 ? '' : 's'}.`);
|
|
78
117
|
}
|
|
@@ -134,6 +173,39 @@ export function wireMessaging(): () => void {
|
|
|
134
173
|
announcer.polite(
|
|
135
174
|
`Site crawl complete. Grade ${msg.report.siteGrade}, ${msg.report.totalUniqueViolations} unique violation${msg.report.totalUniqueViolations === 1 ? '' : 's'}.`
|
|
136
175
|
);
|
|
176
|
+
// rc.236 — Pre-warm resolutions cache for every crawled URL so
|
|
177
|
+
// that when the user clicks a page in the do-the-work report
|
|
178
|
+
// and drills into its Findings, the AI-resolved-fails count is
|
|
179
|
+
// correct on FIRST render (no two-render flicker like rc.235
|
|
180
|
+
// fixed for the now-removed aggregate view). Pre-rc.236 we also
|
|
181
|
+
// flattened all crawled results into the main `results` store
|
|
182
|
+
// to feed an aggregate "View site-wide Findings" button — that
|
|
183
|
+
// button is gone (the Crawl panel itself is the report now),
|
|
184
|
+
// and the aggregate-Findings view's broken math was what
|
|
185
|
+
// confused users. Drill-in setResults still happens, just only
|
|
186
|
+
// when the user clicks a specific page.
|
|
187
|
+
void (async () => {
|
|
188
|
+
try {
|
|
189
|
+
const { loadSiteCrawlPerUrlResults } = await import('../shared/site-crawl-storage');
|
|
190
|
+
const { getResolutionsForPage } = await import('../shared/incomplete-resolutions');
|
|
191
|
+
const perUrl = await loadSiteCrawlPerUrlResults();
|
|
192
|
+
if (!perUrl || perUrl.length === 0) return;
|
|
193
|
+
const setResolutionsForUrl = useStore.getState().setResolutionsForUrl;
|
|
194
|
+
await Promise.all(
|
|
195
|
+
perUrl.map(async (p) => {
|
|
196
|
+
try {
|
|
197
|
+
const r = await getResolutionsForPage(p.url);
|
|
198
|
+
setResolutionsForUrl(p.url, r);
|
|
199
|
+
} catch {
|
|
200
|
+
// best-effort
|
|
201
|
+
}
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
// eslint-disable-next-line no-console
|
|
206
|
+
console.warn('[site-crawl] failed to pre-warm resolutions cache', err);
|
|
207
|
+
}
|
|
208
|
+
})();
|
|
137
209
|
})
|
|
138
210
|
);
|
|
139
211
|
|
|
@@ -150,6 +222,71 @@ export function wireMessaging(): () => void {
|
|
|
150
222
|
return () => offs.forEach((off) => off());
|
|
151
223
|
}
|
|
152
224
|
|
|
225
|
+
/** rc.201 — Auto-export the just-completed audit if the user opted in.
|
|
226
|
+
* Silent + best-effort. Errors are logged but never thrown — the audit
|
|
227
|
+
* itself completed successfully even if file-system write fails. */
|
|
228
|
+
async function maybeAutoExport(
|
|
229
|
+
results: AuditResult[],
|
|
230
|
+
delta: DeltaResult | null | undefined,
|
|
231
|
+
componentId: string | null | undefined,
|
|
232
|
+
): Promise<void> {
|
|
233
|
+
if (results.length === 0) return;
|
|
234
|
+
try {
|
|
235
|
+
const settings = await getAutoExportSettings();
|
|
236
|
+
if (!settings.enabled) return;
|
|
237
|
+
// Tier-gate. Free tier won't get the toggle exposed post-launch;
|
|
238
|
+
// even if a tier change happens between toggle-on and audit-complete,
|
|
239
|
+
// the gate re-checks here.
|
|
240
|
+
const tier = useStore.getState().tier;
|
|
241
|
+
if (isLockoutInEffectForTier(tier)) return;
|
|
242
|
+
if (!isFeatureAllowed(tier, 'autoExportAuditReports')) return;
|
|
243
|
+
|
|
244
|
+
// rc.221 — Loop every enabled format. Each completed audit produces
|
|
245
|
+
// each requested artifact (defense-bundle for legal + ai-prompt for
|
|
246
|
+
// dev handoff + html-print for sharing — pick the set that matches
|
|
247
|
+
// the workflow).
|
|
248
|
+
const url = results[0]?.pageUrl ?? results[0]?.scope ?? '';
|
|
249
|
+
const exported: string[] = [];
|
|
250
|
+
// rc.228 — Snapshot wall-clock now so all formats in this auto-
|
|
251
|
+
// export round see the same value.
|
|
252
|
+
const wallClockMs = useStore.getState().lastAuditWallClockMs ?? undefined;
|
|
253
|
+
for (const format of settings.formats) {
|
|
254
|
+
try {
|
|
255
|
+
const res = await request<ExportResponse>({
|
|
256
|
+
type: 'EXPORT_REQUEST',
|
|
257
|
+
format,
|
|
258
|
+
results,
|
|
259
|
+
delta: delta ?? undefined,
|
|
260
|
+
wallClockMs,
|
|
261
|
+
});
|
|
262
|
+
// EXPORT_REQUEST returns a lockout error string when the SW-side
|
|
263
|
+
// gate refuses. Treat any very-short body as "refused."
|
|
264
|
+
if (typeof res.content !== 'string' || res.content.length < 200) continue;
|
|
265
|
+
const filename = bundleFilename(format, componentId ?? null);
|
|
266
|
+
triggerDownload(res.content, filename);
|
|
267
|
+
if (url) {
|
|
268
|
+
await recordLastAutoExport({
|
|
269
|
+
url,
|
|
270
|
+
exportedAt: new Date().toISOString(),
|
|
271
|
+
format,
|
|
272
|
+
filename,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
exported.push(filename);
|
|
276
|
+
} catch (formatErr) {
|
|
277
|
+
console.warn(`[auto-export] format ${format} failed:`, formatErr);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (exported.length > 0) {
|
|
281
|
+
announcer.polite(`Audit auto-exported ${exported.length} file${exported.length === 1 ? '' : 's'}.`);
|
|
282
|
+
}
|
|
283
|
+
} catch (err) {
|
|
284
|
+
// Silent failure — surface in console for debugging but don't
|
|
285
|
+
// interrupt the user's audit-review flow with a modal.
|
|
286
|
+
console.warn('[auto-export] failed:', err);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
153
290
|
async function persistLastAudit(payload: PersistedAudit): Promise<void> {
|
|
154
291
|
// Strip screenshotDataUrl from each result before persisting. With a full
|
|
155
292
|
// state matrix (up to 144 states), 144 base64 JPEGs at ~50-200KB each blow
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wcag-checkr/ci",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.310",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Headless wcagcheckr accessibility audit runner for CI/CD pipelines. Drives the wcagcheckr Chrome extension via Playwright, runs full-page audits across the state matrix (108 combinations: hover, focus, dark mode, RTL, breakpoints), outputs JSON / SARIF / JUnit, exits with severity-aware codes.",
|
|
6
6
|
"license": "UNLICENSED",
|