legible 0.1.0__tar.gz

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.
legible-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: legible
3
+ Version: 0.1.0
4
+ Summary: Three build gates for things a linter will not catch: type below a readable floor, colour that cannot be read on the surface it sits on, and a retired name reaching anything a human reads.
5
+ Author-email: Vincent Gonzalez <hello@f-keys.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://f-keys.com/legible/
8
+ Project-URL: Source, https://github.com/vince-gonzalez/f-keys/tree/main/gatekit
9
+ Project-URL: Issues, https://github.com/vince-gonzalez/f-keys/issues
10
+ Keywords: accessibility,a11y,wcag,contrast,font-size,ci,build-gate,linter,deadname,rename
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Software Development :: Quality Assurance
17
+ Classifier: Topic :: Software Development :: Testing
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+
21
+ ```
22
+ ╔════════════════════════════════════════════════════════════════════════════════════════════╗
23
+ ║ ║
24
+ ║ ██████╗ █████╗ ████████╗███████╗██╗ ██╗██╗████████╗ ║
25
+ ║ ██╔════╝ ██╔══██╗╚══██╔══╝██╔════╝██║ ██╔╝██║╚══██╔══╝ ║
26
+ ║ ██║ ███╗███████║ ██║ █████╗ █████╔╝ ██║ ██║ ║
27
+ ║ ██║ ██║██╔══██║ ██║ ██╔══╝ ██╔═██╗ ██║ ██║ ║
28
+ ║ ╚██████╔╝██║ ██║ ██║ ███████╗██║ ██╗██║ ██║ ║
29
+ ║ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ║
30
+ ║ ║
31
+ ║ three gates a linter will not give you ║
32
+ ║ ║
33
+ ╚════════════════════════════════════════════════════════════════════════════════════════════╝
34
+ ```
35
+ Three gates for defects a linter does not have an opinion about, because
36
+ none of them is a syntax error. Each one exists because a real person read
37
+ something and could not.
38
+
39
+ | Gate | Fails the build when |
40
+ |---|---|
41
+ | `typefloor` | Type is declared below a readable floor (13px by default) |
42
+ | `contrast-gate` | A declared colour cannot reach 4.5:1 on any surface the file also declares |
43
+ | `deadname-gate` | A retired name reaches anything a human reads |
44
+
45
+ ## Install
46
+
47
+ ```
48
+ pip install legible
49
+ ```
50
+
51
+ ## Use
52
+
53
+ ```
54
+ typefloor ./src # every CSS and inline declaration
55
+ contrast-gate ./src # WCAG 4.5:1 against declared surfaces
56
+ deadname-gate ./src # a name that should be gone
57
+ ```
58
+
59
+ Each exits non-zero on a finding and prints the file and line. `--list`
60
+ shows every offender rather than the summary.
61
+
62
+ ## Why these three
63
+
64
+ **typefloor.** Six-point type is not a style choice. This was written after
65
+ the same reader reported the same thing more than once, and the first time it
66
+ was pointed at a folder it found every violation immediately — what was
67
+ missing was never the tool, it was anything that pulled it.
68
+
69
+ **contrast-gate.** It reads CSS custom properties — `--ink: #hex` against
70
+ `--bg: #hex` — and reports the best surface each ink can reach. Dark grey on
71
+ navy measured 2.64:1 where body text needs 4.5, and a person found that, not
72
+ a build.
73
+
74
+ It does **not** read literal `color:` declarations. A stylesheet using
75
+ neither has nothing to compare, and it says so and exits 0 — which in CI
76
+ reads as a pass on a site nobody checked. Pass `--require-pairs` to fail
77
+ instead when nothing was found. The Action sets it.
78
+
79
+ **deadname-gate.** Fails a build when a name that should be gone reaches a
80
+ title, a footer, a byline, alt text or a comment — while leaving machine
81
+ identifiers alone, because renaming a repository path or a published
82
+ artifact breaks working links or contradicts a deposited record. Both halves
83
+ matter: a gate that cannot tell those apart gets switched off.
84
+
85
+ Written for a company rename. It is the same shape as the problem of a person
86
+ changing their name and finding it in three hundred places, which is the
87
+ harder version and the one worth getting right.
88
+
89
+ ## Exit codes
90
+
91
+ | Code | Means |
92
+ |---|---|
93
+ | `0` | Clean |
94
+ | `1` | It found something. The file and line are printed. |
95
+ | `2` | Misconfigured — it could not check, and will not report clean |
96
+
97
+ The third one matters. `deadname-gate` forbids nothing out of the box, so a
98
+ fresh install exits **2** with an explanation rather than **0**. A gate that
99
+ reports a pass while checking nothing is worse than no gate, and every one of
100
+ these three shipped a version that did exactly that before it was caught.
101
+
102
+ ## Configuration
103
+
104
+ Each gate reads a JSON file of exceptions, so a deliberate choice is
105
+ recorded with a reason rather than silently re-flagged.
106
+
107
+ ```
108
+ typefloor ./src --config typefloor.json
109
+ contrast-gate ./src --config contrast.json
110
+ deadname-gate ./src --config deadname_gate.json
111
+ ```
112
+
113
+ `TYPEFLOOR_CONFIG`, `CONTRAST_CONFIG` and `DEADNAME_CONFIG` do the same.
114
+
115
+ ## In CI
116
+
117
+ ```yaml
118
+ - run: pip install legible
119
+ - run: typefloor ./src
120
+ - run: contrast-gate ./src --require-pairs
121
+ - run: deadname-gate ./src
122
+ ```
123
+
124
+ There are Actions too, in `.github/actions/` of the source repository.
125
+
126
+ ## A note on the argument
127
+
128
+ An earlier version ignored a bare path and scanned its own directory
129
+ instead, then printed `OK`. A gate that reports a pass on a tree it never
130
+ opened is worse than no gate, so a positional path is now the root. If you
131
+ pinned an earlier build, check what it is actually reading.
132
+
133
+ ## Licence
134
+
135
+ MIT.
@@ -0,0 +1,115 @@
1
+ ```
2
+ ╔════════════════════════════════════════════════════════════════════════════════════════════╗
3
+ ║ ║
4
+ ║ ██████╗ █████╗ ████████╗███████╗██╗ ██╗██╗████████╗ ║
5
+ ║ ██╔════╝ ██╔══██╗╚══██╔══╝██╔════╝██║ ██╔╝██║╚══██╔══╝ ║
6
+ ║ ██║ ███╗███████║ ██║ █████╗ █████╔╝ ██║ ██║ ║
7
+ ║ ██║ ██║██╔══██║ ██║ ██╔══╝ ██╔═██╗ ██║ ██║ ║
8
+ ║ ╚██████╔╝██║ ██║ ██║ ███████╗██║ ██╗██║ ██║ ║
9
+ ║ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ║
10
+ ║ ║
11
+ ║ three gates a linter will not give you ║
12
+ ║ ║
13
+ ╚════════════════════════════════════════════════════════════════════════════════════════════╝
14
+ ```
15
+ Three gates for defects a linter does not have an opinion about, because
16
+ none of them is a syntax error. Each one exists because a real person read
17
+ something and could not.
18
+
19
+ | Gate | Fails the build when |
20
+ |---|---|
21
+ | `typefloor` | Type is declared below a readable floor (13px by default) |
22
+ | `contrast-gate` | A declared colour cannot reach 4.5:1 on any surface the file also declares |
23
+ | `deadname-gate` | A retired name reaches anything a human reads |
24
+
25
+ ## Install
26
+
27
+ ```
28
+ pip install legible
29
+ ```
30
+
31
+ ## Use
32
+
33
+ ```
34
+ typefloor ./src # every CSS and inline declaration
35
+ contrast-gate ./src # WCAG 4.5:1 against declared surfaces
36
+ deadname-gate ./src # a name that should be gone
37
+ ```
38
+
39
+ Each exits non-zero on a finding and prints the file and line. `--list`
40
+ shows every offender rather than the summary.
41
+
42
+ ## Why these three
43
+
44
+ **typefloor.** Six-point type is not a style choice. This was written after
45
+ the same reader reported the same thing more than once, and the first time it
46
+ was pointed at a folder it found every violation immediately — what was
47
+ missing was never the tool, it was anything that pulled it.
48
+
49
+ **contrast-gate.** It reads CSS custom properties — `--ink: #hex` against
50
+ `--bg: #hex` — and reports the best surface each ink can reach. Dark grey on
51
+ navy measured 2.64:1 where body text needs 4.5, and a person found that, not
52
+ a build.
53
+
54
+ It does **not** read literal `color:` declarations. A stylesheet using
55
+ neither has nothing to compare, and it says so and exits 0 — which in CI
56
+ reads as a pass on a site nobody checked. Pass `--require-pairs` to fail
57
+ instead when nothing was found. The Action sets it.
58
+
59
+ **deadname-gate.** Fails a build when a name that should be gone reaches a
60
+ title, a footer, a byline, alt text or a comment — while leaving machine
61
+ identifiers alone, because renaming a repository path or a published
62
+ artifact breaks working links or contradicts a deposited record. Both halves
63
+ matter: a gate that cannot tell those apart gets switched off.
64
+
65
+ Written for a company rename. It is the same shape as the problem of a person
66
+ changing their name and finding it in three hundred places, which is the
67
+ harder version and the one worth getting right.
68
+
69
+ ## Exit codes
70
+
71
+ | Code | Means |
72
+ |---|---|
73
+ | `0` | Clean |
74
+ | `1` | It found something. The file and line are printed. |
75
+ | `2` | Misconfigured — it could not check, and will not report clean |
76
+
77
+ The third one matters. `deadname-gate` forbids nothing out of the box, so a
78
+ fresh install exits **2** with an explanation rather than **0**. A gate that
79
+ reports a pass while checking nothing is worse than no gate, and every one of
80
+ these three shipped a version that did exactly that before it was caught.
81
+
82
+ ## Configuration
83
+
84
+ Each gate reads a JSON file of exceptions, so a deliberate choice is
85
+ recorded with a reason rather than silently re-flagged.
86
+
87
+ ```
88
+ typefloor ./src --config typefloor.json
89
+ contrast-gate ./src --config contrast.json
90
+ deadname-gate ./src --config deadname_gate.json
91
+ ```
92
+
93
+ `TYPEFLOOR_CONFIG`, `CONTRAST_CONFIG` and `DEADNAME_CONFIG` do the same.
94
+
95
+ ## In CI
96
+
97
+ ```yaml
98
+ - run: pip install legible
99
+ - run: typefloor ./src
100
+ - run: contrast-gate ./src --require-pairs
101
+ - run: deadname-gate ./src
102
+ ```
103
+
104
+ There are Actions too, in `.github/actions/` of the source repository.
105
+
106
+ ## A note on the argument
107
+
108
+ An earlier version ignored a bare path and scanned its own directory
109
+ instead, then printed `OK`. A gate that reports a pass on a tree it never
110
+ opened is worse than no gate, so a positional path is now the root. If you
111
+ pinned an earlier build, check what it is actually reading.
112
+
113
+ ## Licence
114
+
115
+ MIT.
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "legible"
7
+ version = "0.1.0"
8
+ description = "Three build gates for things a linter will not catch: type below a readable floor, colour that cannot be read on the surface it sits on, and a retired name reaching anything a human reads."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Vincent Gonzalez", email = "hello@f-keys.com" }]
13
+ keywords = [
14
+ "accessibility", "a11y", "wcag", "contrast", "font-size",
15
+ "ci", "build-gate", "linter", "deadname", "rename",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Environment :: Console",
20
+ "Intended Audience :: Developers",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Programming Language :: Python :: 3",
23
+ "Topic :: Software Development :: Quality Assurance",
24
+ "Topic :: Software Development :: Testing",
25
+ ]
26
+ dependencies = []
27
+
28
+ [project.urls]
29
+ Homepage = "https://f-keys.com/legible/"
30
+ Source = "https://github.com/vince-gonzalez/f-keys/tree/main/gatekit"
31
+ Issues = "https://github.com/vince-gonzalez/f-keys/issues"
32
+
33
+ [project.scripts]
34
+ typefloor = "gatekit.typefloor:main"
35
+ contrast-gate = "gatekit.contrast:main"
36
+ deadname-gate = "gatekit.deadname_gate:main"
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["src"]
40
+
41
+ [tool.setuptools.package-data]
42
+ gatekit = ["*.json"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ {
2
+ "floor": 4.5,
3
+ "exceptions": [],
4
+ "_comment": "Exceptions are yours to record, with a reason. The default list is empty on purpose."
5
+ }
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ============================================================
4
+ contrast - nothing on a page may be unreadable
5
+ F-Keys | www.f-keys.com
6
+ ------------------------------------------------------------
7
+ WHY THIS EXISTS
8
+
9
+ "Dark grey on navy" has been reported by the person who has to
10
+ read it more than once, which makes it a class of defect
11
+ rather than a mistake. typefloor.py already stops type being
12
+ too small; this stops it being the wrong colour, by the same
13
+ method: measure it, fail the build, and stop relying on
14
+ anybody noticing.
15
+
16
+ WHAT IT MEASURES
17
+
18
+ The WCAG 2.1 contrast ratio between every colour a stylesheet
19
+ declares and the surface it is drawn on. A ratio is a number
20
+ between 1 (invisible) and 21 (black on white).
21
+
22
+ 4.5 the floor for body text
23
+ 3.0 large text, 24px or 19px bold
24
+
25
+ THE RULE, AND WHY IT IS THIS ONE
26
+
27
+ Comparing every ink against every surface flags pairs that
28
+ never occur together - a green meant for a black readout is
29
+ not a defect because it would be illegible on chrome. So each
30
+ ink has to reach the floor on at least ONE declared surface.
31
+ An ink that fails against all of them has nowhere it can
32
+ legitimately be used, which is exactly the "dark grey on
33
+ navy" case this exists for.
34
+
35
+ WHAT IT CANNOT SEE
36
+
37
+ A colour applied by script at runtime, and a colour whose
38
+ surface is decided by a parent this cannot resolve. Pairings
39
+ are read from the custom properties a file declares, which is
40
+ how these stylesheets are written; anything set inline in JS
41
+ is out of reach and stays that way rather than being guessed
42
+ at.
43
+
44
+ python tools/contrast.py report; exit 1 if any pair fails
45
+ python tools/contrast.py --list every pair, passing or not
46
+ python tools/contrast.py --root remapwrap
47
+
48
+ Pairs that must stay below the floor go in tools/contrast.json
49
+ with a reason, the same as typefloor.
50
+ ============================================================
51
+ """
52
+
53
+ import json
54
+ import os
55
+ import re
56
+ import sys
57
+
58
+ try:
59
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
60
+ except (AttributeError, ValueError):
61
+ pass
62
+
63
+ HERE = os.path.dirname(os.path.abspath(__file__))
64
+ def _config():
65
+ if "--config" in sys.argv:
66
+ i = sys.argv.index("--config")
67
+ if i + 1 < len(sys.argv):
68
+ return os.path.abspath(sys.argv[i + 1])
69
+ return os.environ.get("CONTRAST_CONFIG") or os.path.join(HERE, "contrast.json")
70
+
71
+
72
+ CONFIG = _config()
73
+
74
+ #: Which variables name a surface something is drawn on. Everything else
75
+ #: that looks like a colour is treated as ink.
76
+ SURFACES = ("bg", "background", "panel", "surface", "paper", "card",
77
+ "field", "sel", "face", "desktop", "title-b")
78
+
79
+ SKIP_DIRS = {"node_modules", ".git", "dist", "build", "__pycache__",
80
+ "vendor", ".venv", "fonts", "assets"}
81
+
82
+
83
+ def load_config():
84
+ if not os.path.exists(CONFIG):
85
+ return {"floor": 4.5, "exceptions": []}
86
+ with open(CONFIG, encoding="utf-8") as fh:
87
+ cfg = json.load(fh)
88
+ cfg.setdefault("floor", 4.5)
89
+ cfg.setdefault("exceptions", [])
90
+ return cfg
91
+
92
+
93
+ def to_rgb(value):
94
+ v = value.strip().lower()
95
+ m = re.match(r"^#([0-9a-f]{3,8})$", v)
96
+ if m:
97
+ h = m.group(1)
98
+ if len(h) == 3:
99
+ h = "".join(c * 2 for c in h)
100
+ if len(h) < 6:
101
+ return None
102
+ return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))
103
+ m = re.match(r"^rgba?\(([^)]+)\)$", v)
104
+ if m:
105
+ parts = [p.strip() for p in m.group(1).replace("/", ",").split(",")]
106
+ try:
107
+ return tuple(int(float(p)) for p in parts[:3])
108
+ except ValueError:
109
+ return None
110
+ return None
111
+
112
+
113
+ def luminance(rgb):
114
+ out = []
115
+ for c in rgb:
116
+ c = c / 255.0
117
+ out.append(c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4)
118
+ return 0.2126 * out[0] + 0.7152 * out[1] + 0.0722 * out[2]
119
+
120
+
121
+ def ratio(a, b):
122
+ la, lb = luminance(a), luminance(b)
123
+ hi, lo = max(la, lb), min(la, lb)
124
+ return (hi + 0.05) / (lo + 0.05)
125
+
126
+
127
+ def variables(text):
128
+ """Every --name: value the file declares."""
129
+ out = {}
130
+ for name, value in re.findall(r"--([\w-]+)\s*:\s*([^;}]+)[;}]", text):
131
+ rgb = to_rgb(value)
132
+ if rgb:
133
+ out[name] = rgb
134
+ return out
135
+
136
+
137
+ def walk(root):
138
+ for base, dirs, files in os.walk(root):
139
+ dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith(".")]
140
+ for name in files:
141
+ if name.lower().endswith((".html", ".css")):
142
+ yield os.path.join(base, name)
143
+
144
+
145
+ def check(root, cfg):
146
+ floor = cfg["floor"]
147
+ allowed = {(e.get("ink"), e.get("on")) for e in cfg["exceptions"]}
148
+ rows = []
149
+
150
+ for path in walk(root):
151
+ try:
152
+ text = open(path, encoding="utf-8", errors="replace").read()
153
+ except OSError:
154
+ continue
155
+ vars_ = variables(text)
156
+ if not vars_:
157
+ continue
158
+
159
+ surfaces = {n: c for n, c in vars_.items()
160
+ if any(s in n for s in SURFACES)}
161
+ inks = {n: c for n, c in vars_.items()
162
+ if n not in surfaces and not n.startswith("glow")}
163
+ if not surfaces or not inks:
164
+ continue
165
+
166
+ rel = os.path.relpath(path, root).replace("\\", "/")
167
+ for ink_name, ink in sorted(inks.items()):
168
+ # The best surface this ink has. An ink that reaches the floor
169
+ # somewhere has a place it belongs; one that reaches it nowhere
170
+ # cannot be read against anything the file declares.
171
+ best_name, best = None, 0.0
172
+ for surf_name, surf in sorted(surfaces.items()):
173
+ r = ratio(ink, surf)
174
+ if r > best:
175
+ best_name, best = surf_name, r
176
+ declared = any((ink_name, s) in allowed for s in surfaces)
177
+ rows.append({"file": rel, "ink": ink_name, "on": best_name,
178
+ "ratio": best,
179
+ "ok": best >= floor or declared,
180
+ "declared": declared})
181
+ return rows
182
+
183
+
184
+ def _help():
185
+ print("""contrast-gate - Fail a build when a declared colour cannot be read on any surface the file declares.
186
+
187
+ usage:
188
+ contrast-gate [PATH] [--config FILE] [--list] [--require-pairs]
189
+
190
+ PATH directory to check (default: the working directory)
191
+ --config JSON file of recorded exceptions
192
+ --list every pair rather than the summary
193
+ --require-pairs fail if no colour pair was found at all, instead of
194
+ passing vacuously on a stylesheet with no variables
195
+ --root DIR same as PATH, kept for existing callers
196
+
197
+ This reads CSS custom properties (--name: #hex), not literal
198
+ color/background declarations.
199
+
200
+ Exits 0 when clean, 1 on a finding. Part of readable-gates.
201
+ https://f-keys.com/gatekit/""")
202
+ return 0
203
+
204
+
205
+ def main():
206
+ if "--help" in sys.argv or "-h" in sys.argv:
207
+ return _help()
208
+ args = sys.argv[1:]
209
+ # Packaged, the directory above this file is site-packages,
210
+ # not the project being checked. The working directory is
211
+ # what someone running this in their repo means.
212
+ root = os.environ.get("CONTRAST_ROOT") or os.getcwd()
213
+ if "--root" in args:
214
+ root = os.path.abspath(args[args.index("--root") + 1])
215
+ else:
216
+ # a bare path was silently ignored, which meant this scanned its
217
+ # own repository and reported OK on a tree nobody asked about
218
+ positional = [a for a in args if not a.startswith("-")]
219
+ if positional:
220
+ root = os.path.abspath(positional[0])
221
+
222
+ cfg = load_config()
223
+ rows = check(root, cfg)
224
+ if not rows:
225
+ # This reads CSS custom properties - `--ink: #hex` against
226
+ # `--bg: #hex` - not literal `color:` declarations. A stylesheet
227
+ # that uses neither has nothing here to compare, which is a true
228
+ # statement and a dangerous one: in CI it exits 0 and reads as a
229
+ # pass on a site nobody checked. --require-pairs is how you say
230
+ # "I expect this to find something", and the Action sets it.
231
+ print(" contrast: nothing with declared colours to check.")
232
+ print(" This audits CSS custom properties (--name: #hex),")
233
+ print(" not literal color/background declarations.")
234
+ if "--require-pairs" in args:
235
+ print(" contrast: FAILED - --require-pairs was set and no pair "
236
+ "was found.")
237
+ return 1
238
+ return 0
239
+
240
+ if "--list" in args:
241
+ for r in sorted(rows, key=lambda x: x["ratio"]):
242
+ mark = "ok " if r["ok"] else "FAIL"
243
+ print(" {} {:>5.2f} --{} at best on --{} {}".format(
244
+ mark, r["ratio"], r["ink"], r["on"], r["file"]))
245
+ return 0
246
+
247
+ bad = [r for r in rows if not r["ok"]]
248
+ if not bad:
249
+ print(" contrast: OK - every declared pair reaches {}:1"
250
+ .format(cfg["floor"]))
251
+ return 0
252
+
253
+ print(" contrast: {} pair(s) below {}:1 and not declared\n"
254
+ .format(len(bad), cfg["floor"]))
255
+ for r in sorted(bad, key=lambda x: x["ratio"])[:40]:
256
+ print(" {:>5.2f} --{} has no surface it reads on (best is --{}) {}".format(
257
+ r["ratio"], r["ink"], r["on"], r["file"]))
258
+ print("\n Either change the colours, or add the pair to tools/"
259
+ "contrast.json with a reason.")
260
+ return 1
261
+
262
+
263
+ if __name__ == "__main__":
264
+ sys.exit(main())
@@ -0,0 +1,13 @@
1
+ {
2
+ "_comment": [
3
+ "Names that must not reach anything a human reads.",
4
+ "'forbidden' is a list of case-insensitive words.",
5
+ "'allowed' exempts machine identifiers - repo paths, package names,",
6
+ "live URLs - because renaming those breaks working links or",
7
+ "contradicts a published record. Each needs a reason.",
8
+ "Nothing is forbidden by default: say what yours is."
9
+ ],
10
+ "forbidden": [],
11
+ "allowed": [],
12
+ "skip_paths": []
13
+ }