@xenos1996/usa 2.0.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/LICENSE +21 -0
- package/README.md +335 -0
- package/USA.md +597 -0
- package/dist/bootstrap/catalog.d.ts +46 -0
- package/dist/bootstrap/catalog.d.ts.map +1 -0
- package/dist/bootstrap/catalog.js +312 -0
- package/dist/bootstrap/catalog.js.map +1 -0
- package/dist/bootstrap/index.d.ts +36 -0
- package/dist/bootstrap/index.d.ts.map +1 -0
- package/dist/bootstrap/index.js +177 -0
- package/dist/bootstrap/index.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +706 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +88 -0
- package/dist/config.js.map +1 -0
- package/dist/detect/index.d.ts +58 -0
- package/dist/detect/index.d.ts.map +1 -0
- package/dist/detect/index.js +329 -0
- package/dist/detect/index.js.map +1 -0
- package/dist/engine/audit.d.ts +32 -0
- package/dist/engine/audit.d.ts.map +1 -0
- package/dist/engine/audit.js +366 -0
- package/dist/engine/audit.js.map +1 -0
- package/dist/engine/diff.d.ts +24 -0
- package/dist/engine/diff.d.ts.map +1 -0
- package/dist/engine/diff.js +136 -0
- package/dist/engine/diff.js.map +1 -0
- package/dist/engine/evaluate.d.ts +19 -0
- package/dist/engine/evaluate.d.ts.map +1 -0
- package/dist/engine/evaluate.js +377 -0
- package/dist/engine/evaluate.js.map +1 -0
- package/dist/engine/gate.d.ts +19 -0
- package/dist/engine/gate.d.ts.map +1 -0
- package/dist/engine/gate.js +47 -0
- package/dist/engine/gate.js.map +1 -0
- package/dist/engine/loader.d.ts +37 -0
- package/dist/engine/loader.d.ts.map +1 -0
- package/dist/engine/loader.js +388 -0
- package/dist/engine/loader.js.map +1 -0
- package/dist/engine/maturity.d.ts +20 -0
- package/dist/engine/maturity.d.ts.map +1 -0
- package/dist/engine/maturity.js +157 -0
- package/dist/engine/maturity.js.map +1 -0
- package/dist/engine/score.d.ts +19 -0
- package/dist/engine/score.d.ts.map +1 -0
- package/dist/engine/score.js +141 -0
- package/dist/engine/score.js.map +1 -0
- package/dist/engine/sections.d.ts +14 -0
- package/dist/engine/sections.d.ts.map +1 -0
- package/dist/engine/sections.js +50 -0
- package/dist/engine/sections.js.map +1 -0
- package/dist/evolution/benchmark.d.ts +12 -0
- package/dist/evolution/benchmark.d.ts.map +1 -0
- package/dist/evolution/benchmark.js +107 -0
- package/dist/evolution/benchmark.js.map +1 -0
- package/dist/evolution/capability.d.ts +22 -0
- package/dist/evolution/capability.d.ts.map +1 -0
- package/dist/evolution/capability.js +69 -0
- package/dist/evolution/capability.js.map +1 -0
- package/dist/evolution/coverage.d.ts +15 -0
- package/dist/evolution/coverage.d.ts.map +1 -0
- package/dist/evolution/coverage.js +65 -0
- package/dist/evolution/coverage.js.map +1 -0
- package/dist/evolution/gap.d.ts +10 -0
- package/dist/evolution/gap.d.ts.map +1 -0
- package/dist/evolution/gap.js +52 -0
- package/dist/evolution/gap.js.map +1 -0
- package/dist/evolution/propose.d.ts +50 -0
- package/dist/evolution/propose.d.ts.map +1 -0
- package/dist/evolution/propose.js +188 -0
- package/dist/evolution/propose.js.map +1 -0
- package/dist/evolution/queue.d.ts +64 -0
- package/dist/evolution/queue.d.ts.map +1 -0
- package/dist/evolution/queue.js +192 -0
- package/dist/evolution/queue.js.map +1 -0
- package/dist/evolution/release.d.ts +11 -0
- package/dist/evolution/release.d.ts.map +1 -0
- package/dist/evolution/release.js +55 -0
- package/dist/evolution/release.js.map +1 -0
- package/dist/evolution/run.d.ts +88 -0
- package/dist/evolution/run.d.ts.map +1 -0
- package/dist/evolution/run.js +258 -0
- package/dist/evolution/run.js.map +1 -0
- package/dist/evolution/schedule.d.ts +59 -0
- package/dist/evolution/schedule.d.ts.map +1 -0
- package/dist/evolution/schedule.js +77 -0
- package/dist/evolution/schedule.js.map +1 -0
- package/dist/evolution/types.d.ts +174 -0
- package/dist/evolution/types.d.ts.map +1 -0
- package/dist/evolution/types.js +8 -0
- package/dist/evolution/types.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/learn/index.d.ts +27 -0
- package/dist/learn/index.d.ts.map +1 -0
- package/dist/learn/index.js +196 -0
- package/dist/learn/index.js.map +1 -0
- package/dist/report/markdown.d.ts +16 -0
- package/dist/report/markdown.d.ts.map +1 -0
- package/dist/report/markdown.js +488 -0
- package/dist/report/markdown.js.map +1 -0
- package/dist/snapshot/index.d.ts +55 -0
- package/dist/snapshot/index.d.ts.map +1 -0
- package/dist/snapshot/index.js +76 -0
- package/dist/snapshot/index.js.map +1 -0
- package/dist/store/index.d.ts +31 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +69 -0
- package/dist/store/index.js.map +1 -0
- package/dist/types.d.ts +287 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/util/glob.d.ts +11 -0
- package/dist/util/glob.d.ts.map +1 -0
- package/dist/util/glob.js +86 -0
- package/dist/util/glob.js.map +1 -0
- package/dist/util/project.d.ts +90 -0
- package/dist/util/project.d.ts.map +1 -0
- package/dist/util/project.js +636 -0
- package/dist/util/project.js.map +1 -0
- package/dist/util/yaml.d.ts +28 -0
- package/dist/util/yaml.d.ts.map +1 -0
- package/dist/util/yaml.js +48 -0
- package/dist/util/yaml.js.map +1 -0
- package/package.json +94 -0
- package/rules/core/architecture.yaml +190 -0
- package/rules/core/cicd.yaml +212 -0
- package/rules/core/code-quality.yaml +261 -0
- package/rules/core/dependencies.yaml +132 -0
- package/rules/core/documentation.yaml +119 -0
- package/rules/core/future-readiness.yaml +91 -0
- package/rules/core/release.yaml +131 -0
- package/rules/core/repo.yaml +313 -0
- package/rules/core/security.yaml +496 -0
- package/rules/core/supply-chain.yaml +357 -0
- package/rules/core/testing.yaml +266 -0
- package/rules/detectors.yaml +2028 -0
- package/rules/index.yaml +42 -0
- package/rules/profiles/maturity.yaml +135 -0
- package/rules/stacks/ai-era.yaml +193 -0
- package/rules/stacks/api-backend.yaml +121 -0
- package/rules/stacks/cli.yaml +105 -0
- package/rules/stacks/compliance.yaml +157 -0
- package/rules/stacks/containers.yaml +124 -0
- package/rules/stacks/data.yaml +165 -0
- package/rules/stacks/go.yaml +134 -0
- package/rules/stacks/iac.yaml +128 -0
- package/rules/stacks/jvm.yaml +150 -0
- package/rules/stacks/ml-ai.yaml +140 -0
- package/rules/stacks/mobile.yaml +121 -0
- package/rules/stacks/node-typescript.yaml +166 -0
- package/rules/stacks/python.yaml +175 -0
- package/rules/stacks/rust.yaml +129 -0
- package/rules/stacks/solidity.yaml +139 -0
- package/rules/stacks/swift.yaml +100 -0
- package/rules/stacks/web-frontend.yaml +159 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
id: stacks/swift
|
|
2
|
+
title: Swift
|
|
3
|
+
section: S15
|
|
4
|
+
section_title: Platform-Specific
|
|
5
|
+
description: 'Activated when Swift is detected. Graduated from a bootstrap proposal proven against vapor/vapor — see ADR-0012.'
|
|
6
|
+
version: '1.0'
|
|
7
|
+
skip_when:
|
|
8
|
+
all:
|
|
9
|
+
- { fact: 'lang:swift', op: 'absent' }
|
|
10
|
+
|
|
11
|
+
rules:
|
|
12
|
+
- id: SW-001
|
|
13
|
+
title: No force-unwrap or force-try in application code
|
|
14
|
+
section: S15
|
|
15
|
+
section_title: Platform-Specific
|
|
16
|
+
severity: MEDIUM
|
|
17
|
+
class: correctness
|
|
18
|
+
check:
|
|
19
|
+
kind: grep_wrong
|
|
20
|
+
pattern: '(try!\\s|as!\\s)'
|
|
21
|
+
include: ['**/*.swift']
|
|
22
|
+
exclude: ['**/*.test.*', '**/*.spec.*', '**/test/**', '**/Tests/**']
|
|
23
|
+
flags: i
|
|
24
|
+
why: 'try!/as! turn recoverable failures into crashes. Tests may keep them; shipped code should not.'
|
|
25
|
+
remediation: 'Handle errors with do/catch or propagate with try; replace as! with as? plus a guard.'
|
|
26
|
+
references: ['CWE-754']
|
|
27
|
+
|
|
28
|
+
- id: SW-002
|
|
29
|
+
title: App Transport Security is not disabled
|
|
30
|
+
section: S15
|
|
31
|
+
section_title: Platform-Specific
|
|
32
|
+
severity: HIGH
|
|
33
|
+
class: security
|
|
34
|
+
check:
|
|
35
|
+
kind: grep_wrong
|
|
36
|
+
pattern: '(NSAllowsArbitraryLoads|NSExceptionAllowsInsecureHTTPLoads)'
|
|
37
|
+
include: ['**/*.plist', '**/*.swift']
|
|
38
|
+
exclude: ['**/*.test.*', '**/*.spec.*']
|
|
39
|
+
flags: i
|
|
40
|
+
why: 'Disabling ATS reopens plaintext HTTP for the whole app or exception domains.'
|
|
41
|
+
remediation: 'Remove the exception keys; pin exception domains narrowly with justification if truly required.'
|
|
42
|
+
references: ['CWE-319']
|
|
43
|
+
|
|
44
|
+
- id: SW-003
|
|
45
|
+
title: Application dependencies are pinned
|
|
46
|
+
section: S15
|
|
47
|
+
section_title: Platform-Specific
|
|
48
|
+
severity: MEDIUM
|
|
49
|
+
class: supply-chain
|
|
50
|
+
applies_when: { fact: 'swift:executable' }
|
|
51
|
+
check:
|
|
52
|
+
kind: file_exists
|
|
53
|
+
files: ['Package.resolved']
|
|
54
|
+
why: 'An unpinned Package.swift resolves newest compatibles at build time. Scoped to executable targets: libraries must NOT commit Package.resolved (the resolver ignores it for library products).'
|
|
55
|
+
remediation: 'Commit Package.resolved so every build resolves identically.'
|
|
56
|
+
references: ['CWE-829']
|
|
57
|
+
|
|
58
|
+
- id: SW-004
|
|
59
|
+
title: Secrets live in the Keychain with data protection
|
|
60
|
+
section: S15
|
|
61
|
+
section_title: Platform-Specific
|
|
62
|
+
severity: HIGH
|
|
63
|
+
class: security
|
|
64
|
+
check:
|
|
65
|
+
kind: manual
|
|
66
|
+
why: 'UserDefaults/plist storage of tokens and missing file-protection classes need a human to confirm.'
|
|
67
|
+
remediation: 'Store credentials via SecItem with kSecAttrAccessibleWhenUnlockedThisDeviceOnly or stronger; set NSFileProtectionComplete.'
|
|
68
|
+
evidence: 'SecItem call sites file:line, UserDefaults keys inventory, file-protection settings.'
|
|
69
|
+
references: ['ASVS-2.4.1', 'MASVS-STORAGE']
|
|
70
|
+
|
|
71
|
+
- id: SW-005
|
|
72
|
+
title: Request validation uses Validatable or a validation library
|
|
73
|
+
section: S2
|
|
74
|
+
section_title: Security
|
|
75
|
+
severity: HIGH
|
|
76
|
+
class: security
|
|
77
|
+
applies_when: { fact: 'fw:vapor' }
|
|
78
|
+
check:
|
|
79
|
+
kind: grep_present
|
|
80
|
+
pattern: 'Validatable|import Validation'
|
|
81
|
+
include: ['**/*.swift']
|
|
82
|
+
exclude: ['**/*.test.*', '**/Tests/**']
|
|
83
|
+
why: 'Without schema validation at the edge, untrusted input can cause injection, crashes, or unexpected behavior.'
|
|
84
|
+
remediation: 'Conform request bodies to Validatable and call validate() before use.'
|
|
85
|
+
references: ['CWE-20', 'ASVS-5.1.1']
|
|
86
|
+
|
|
87
|
+
- id: SW-006
|
|
88
|
+
title: Security headers middleware is configured
|
|
89
|
+
section: S2
|
|
90
|
+
section_title: Security
|
|
91
|
+
severity: MEDIUM
|
|
92
|
+
class: security
|
|
93
|
+
applies_when: { fact: 'fw:vapor' }
|
|
94
|
+
check:
|
|
95
|
+
kind: grep_present
|
|
96
|
+
pattern: 'SecurityHeadersMiddleware|CORSMiddleware'
|
|
97
|
+
include: ['**/*.swift']
|
|
98
|
+
why: 'Missing security headers exposes the app to clickjacking, MIME sniffing, and other browser attacks.'
|
|
99
|
+
remediation: 'Add SecurityHeadersMiddleware to the middleware chain with appropriate policies.'
|
|
100
|
+
references: ['ASVS-14.4.3', 'OWASP-Secure-Headers']
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
id: stacks/web-frontend
|
|
2
|
+
title: Web Frontend
|
|
3
|
+
section: S15
|
|
4
|
+
section_title: Platform-Specific
|
|
5
|
+
description: 'Activated for browser-facing applications. Covers Core Web Vitals, a11y, and bundle discipline.'
|
|
6
|
+
version: '1.0'
|
|
7
|
+
skip_when:
|
|
8
|
+
all:
|
|
9
|
+
- { fact: 'platform:web', op: 'absent' }
|
|
10
|
+
- { fact: 'fw:react', op: 'absent' }
|
|
11
|
+
- { fact: 'fw:vue', op: 'absent' }
|
|
12
|
+
- { fact: 'fw:svelte', op: 'absent' }
|
|
13
|
+
- { fact: 'fw:angular', op: 'absent' }
|
|
14
|
+
- { fact: 'fw:next', op: 'absent' }
|
|
15
|
+
- { fact: 'fw:nuxt', op: 'absent' }
|
|
16
|
+
- { fact: 'fw:sveltekit', op: 'absent' }
|
|
17
|
+
- { fact: 'fw:astro', op: 'absent' }
|
|
18
|
+
|
|
19
|
+
rules:
|
|
20
|
+
- id: WEB-001
|
|
21
|
+
title: Core Web Vitals are measured
|
|
22
|
+
section: S15
|
|
23
|
+
section_title: Platform-Specific
|
|
24
|
+
severity: MEDIUM
|
|
25
|
+
class: performance
|
|
26
|
+
check:
|
|
27
|
+
kind: grep_present
|
|
28
|
+
pattern: '(web-vitals|LCP|CLS|INP|lighthouse|next/web-vitals|useReportWebVitals|reportWebVitals)'
|
|
29
|
+
include:
|
|
30
|
+
['**/*.{ts,tsx,js,jsx,html}', 'package.json', '.github/workflows/*.yml', 'lighthouse*']
|
|
31
|
+
exclude: ['**/node_modules/**']
|
|
32
|
+
flags: i
|
|
33
|
+
why: 'You cannot improve what you do not measure, and LCP/INP/CLS are what search and users actually judge.'
|
|
34
|
+
remediation: 'Report web-vitals to your analytics; add Lighthouse CI to catch regressions per PR.'
|
|
35
|
+
references: ['Web-Vitals']
|
|
36
|
+
|
|
37
|
+
- id: WEB-002
|
|
38
|
+
title: Code splitting / lazy loading is used
|
|
39
|
+
section: S15
|
|
40
|
+
section_title: Platform-Specific
|
|
41
|
+
severity: MEDIUM
|
|
42
|
+
class: performance
|
|
43
|
+
check:
|
|
44
|
+
kind: grep_present
|
|
45
|
+
pattern: '(React\.lazy|import\(\s*[''"]|dynamic\s*\(\s*\(\s*\)|defineAsyncComponent|loadable|next/dynamic|Suspense)'
|
|
46
|
+
include: ['**/*.{ts,tsx,js,jsx,vue,svelte}']
|
|
47
|
+
exclude: ['**/node_modules/**', '**/*.test.*']
|
|
48
|
+
why: 'A single 2 MB bundle means every user pays for the heaviest route on first load.'
|
|
49
|
+
remediation: 'Route-level code splitting first, then lazy-load anything behind a tab, modal, or fold.'
|
|
50
|
+
references: ['Web-Vitals:LCP']
|
|
51
|
+
|
|
52
|
+
- id: WEB-003
|
|
53
|
+
title: Images are optimised
|
|
54
|
+
section: S15
|
|
55
|
+
section_title: Platform-Specific
|
|
56
|
+
severity: MEDIUM
|
|
57
|
+
class: performance
|
|
58
|
+
check:
|
|
59
|
+
kind: grep_present
|
|
60
|
+
pattern: '(loading\s*=\s*[''"]lazy|next/image|next/font|<Image|webp|avif|decoding\s*=\s*[''"]async)'
|
|
61
|
+
include: ['**/*.{ts,tsx,js,jsx,vue,svelte,html,astro}']
|
|
62
|
+
exclude: ['**/node_modules/**']
|
|
63
|
+
flags: i
|
|
64
|
+
why: 'Images are usually 60%+ of page weight. AVIF/WebP plus lazy loading is the single biggest win available.'
|
|
65
|
+
remediation: 'Serve WebP/AVIF at the rendered size; lazy-load below the fold; set width/height to avoid CLS.'
|
|
66
|
+
references: ['Web-Vitals:LCP', 'Web-Vitals:CLS']
|
|
67
|
+
|
|
68
|
+
- id: WEB-004
|
|
69
|
+
title: Accessibility basics are in place
|
|
70
|
+
section: S15
|
|
71
|
+
section_title: Platform-Specific
|
|
72
|
+
severity: MEDIUM
|
|
73
|
+
class: compliance
|
|
74
|
+
check: { kind: manual }
|
|
75
|
+
evidence: 'Lighthouse/axe accessibility score, or evidence of: semantic landmarks, labelled form inputs, visible focus, and alt text on meaningful images.'
|
|
76
|
+
why: 'Roughly 1 in 4 adults has a disability. Accessibility bugs are also, almost always, plain bugs.'
|
|
77
|
+
remediation: 'Add eslint-plugin-jsx-a11y; run axe in CI; test keyboard-only navigation end to end.'
|
|
78
|
+
references: ['WCAG-2.2-AA', 'EN-301-549', 'ADA']
|
|
79
|
+
|
|
80
|
+
- id: WEB-005
|
|
81
|
+
title: Bundle size is monitored
|
|
82
|
+
section: S15
|
|
83
|
+
section_title: Platform-Specific
|
|
84
|
+
severity: LOW
|
|
85
|
+
class: performance
|
|
86
|
+
check:
|
|
87
|
+
kind: grep_present
|
|
88
|
+
pattern: '(size-limit|bundlesize|rollup-plugin-visualizer|webpack-bundle-analyzer|bundlephobia|build\.analyze|"size-limit")'
|
|
89
|
+
include:
|
|
90
|
+
[
|
|
91
|
+
'package.json',
|
|
92
|
+
'.github/workflows/*.yml',
|
|
93
|
+
'vite.config.*',
|
|
94
|
+
'next.config.*',
|
|
95
|
+
'webpack.config.*',
|
|
96
|
+
]
|
|
97
|
+
exclude: ['**/node_modules/**']
|
|
98
|
+
flags: i
|
|
99
|
+
why: 'Bundle size grows by 5% a sprint and nobody notices until it is 3 MB.'
|
|
100
|
+
remediation: 'Add size-limit with a per-entry budget; fail CI when the budget is exceeded.'
|
|
101
|
+
references: ['Web-Vitals']
|
|
102
|
+
|
|
103
|
+
- id: WEB-006
|
|
104
|
+
title: Error boundaries exist around risky UI
|
|
105
|
+
section: S15
|
|
106
|
+
section_title: Platform-Specific
|
|
107
|
+
severity: MEDIUM
|
|
108
|
+
class: correctness
|
|
109
|
+
applies_when:
|
|
110
|
+
any:
|
|
111
|
+
- { fact: 'fw:react' }
|
|
112
|
+
- { fact: 'fw:next' }
|
|
113
|
+
check:
|
|
114
|
+
kind: grep_present
|
|
115
|
+
pattern: '(ErrorBoundary|componentDidCatch|error\.tsx|error-boundary|useErrorBoundary)'
|
|
116
|
+
include: ['**/*.{ts,tsx,js,jsx}']
|
|
117
|
+
exclude: ['**/node_modules/**', '**/*.test.*']
|
|
118
|
+
why: 'One throw inside a render and the user gets a blank white page.'
|
|
119
|
+
remediation: 'Wrap route segments in an ErrorBoundary with a retry and a report-to-Sentry path.'
|
|
120
|
+
references: ['React-Error-Boundaries']
|
|
121
|
+
|
|
122
|
+
- id: WEB-007
|
|
123
|
+
title: No secrets in client-side bundles
|
|
124
|
+
section: S15
|
|
125
|
+
section_title: Platform-Specific
|
|
126
|
+
severity: CRITICAL
|
|
127
|
+
class: security
|
|
128
|
+
check:
|
|
129
|
+
kind: grep_wrong
|
|
130
|
+
pattern: '(NEXT_PUBLIC_.*(SECRET|KEY|TOKEN|PASSWORD)|VITE_.*(SECRET|PRIVATE|PASSWORD)|process\.env\.[A-Z_]*(SECRET|PRIVATE_KEY|PASSWORD)[A-Z_]*)'
|
|
131
|
+
include: ['**/*.{ts,tsx,js,jsx,vue,svelte}']
|
|
132
|
+
exclude: ['**/node_modules/**', '**/*.test.*', '**/server/**', '**/api/**']
|
|
133
|
+
why: 'Anything bundled for the browser is public. NEXT_PUBLIC_/VITE_ variables are shipped verbatim.'
|
|
134
|
+
remediation: 'Move the call server-side (API route, server action, BFF) and keep the secret out of the bundle.'
|
|
135
|
+
references: ['CWE-798', 'ASVS-14.3.2']
|
|
136
|
+
|
|
137
|
+
- id: WEB-008
|
|
138
|
+
title: Forms validate and expose errors accessibly
|
|
139
|
+
section: S15
|
|
140
|
+
section_title: Platform-Specific
|
|
141
|
+
severity: MEDIUM
|
|
142
|
+
class: compliance
|
|
143
|
+
check: { kind: manual }
|
|
144
|
+
evidence: 'A form with: a validation schema, error text linked via aria-describedby, and focus moved to the first error.'
|
|
145
|
+
why: 'An error message only sighted users can see (or only sighted users can find) is not an error message.'
|
|
146
|
+
remediation: 'Use a schema validator, render errors next to the field, and set aria-invalid / aria-describedby.'
|
|
147
|
+
references: ['WCAG-3.3.1', 'WCAG-3.3.3']
|
|
148
|
+
|
|
149
|
+
- id: WEB-009
|
|
150
|
+
title: An offline/PWA story exists
|
|
151
|
+
section: S15
|
|
152
|
+
section_title: Platform-Specific
|
|
153
|
+
severity: FUTURE
|
|
154
|
+
class: performance
|
|
155
|
+
check: { kind: manual }
|
|
156
|
+
evidence: 'Service worker, offline fallback page, or an explicit decision that offline support is not required.'
|
|
157
|
+
why: 'Mobile networks fail. Deciding not to support offline is fine; discovering it during an outage is not.'
|
|
158
|
+
remediation: 'At minimum, add a custom offline page. Full PWA only if the use case justifies it.'
|
|
159
|
+
references: ['Web-Vitals']
|