getreadystack commited on
Commit
f594d3d
·
verified ·
1 Parent(s): c27fdb6

Upload index.html with huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +404 -17
index.html CHANGED
@@ -1,19 +1,406 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>pyproject.toml Release Gate (PEP 639) — free online check</title>
7
+ <style>
8
+ :root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512}
9
+ .wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px}
10
+ textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box}
11
+ .bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer}
12
+ input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px}
13
+ table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65}
14
+ .sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600}
15
+ .note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa}
16
+ </style>
17
+ <script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
18
+ <!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/pyproject-release-gate"></head>
19
+ <body>
20
+ <div class="wrap">
21
+ <h1>pyproject.toml Release Gate (PEP 639)</h1>
22
+ <p class="lede">Catches pyproject.toml metadata that breaks your next release: end-of-life requires-python, PEP 639 licence drift, classifier conflicts, setuptools too old for SPDX. Date-aware, offline. Runs entirely in your browser — nothing is uploaded.</p>
23
+ <textarea id="src" spellcheck="false"># pyproject.toml as an assistant wrote it in one pass
24
+ [build-system]
25
+ requires = ["setuptools&gt;=61.0", "wheel"]
26
+ build-backend = "setuptools.build_meta"
27
+
28
+ [project]
29
+ name = "Invoice_Toolkit"
30
+ version = "1.4.0"
31
+ description = "Helpers for parsing and validating supplier invoices"
32
+ requires-python = "&gt;=3.8,&lt;4"
33
+ license = "Apache 2.0"
34
+ license-files = ["/usr/share/licenses/invoice-toolkit/LICENSE", "../COPYING"]
35
+ dynamic = ["version"]
36
+ authors = [{ name = "A. Maintainer", email = "a@example.com" }]
37
+ dependencies = [
38
+ "httpx&gt;=0.27",
39
+ "pydantic&gt;=2.6",
40
+ ]
41
+ classifiers = [
42
+ "Development Status :: 4 - Beta",
43
+ "Programming Language :: Python :: 3.7",
44
+ "Programming Language :: Python :: 3.8",
45
+ "Programming Language :: Python :: 3.12",
46
+ "License :: OSI Approved :: Apache Software License",
47
+ "Operating System :: OS Independent",
48
+ ]</textarea>
49
+ <div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div>
50
+ <div id="out" aria-live="polite"></div>
51
+ <div id="paid"><strong>Free here:</strong> Every finding for the pyproject.toml open in your editor, with the line, the spec that changed and the replacement text. <strong>In the editor:</strong> Sweeping every pyproject.toml in a monorepo in one pass and writing a dated report file you keep in the repo — that part asks for a licence key.</div>
52
+ <p class="note">Same engine as the VS Code extension, byte for byte.</p>
53
+ </div>
54
+ <script>var PJ_RULES = {
55
+ "meta": {
56
+ "id": "pyproject-release-gate",
57
+ "rule_set_version": "2026.09",
58
+ "scope": "[project] metadata in pyproject.toml (PEP 621 + PEP 639)",
59
+ "yardstick": "A packaging consultant reviewing one release manifest bills by the hour; this reads the same file in under a second."
60
+ },
61
+ "python_eol": {
62
+ "3.6": "2021-12-23",
63
+ "3.7": "2023-06-27",
64
+ "3.8": "2024-10-07",
65
+ "3.9": "2025-10-31",
66
+ "3.10": "2026-10-31",
67
+ "3.11": "2027-10-31",
68
+ "3.12": "2028-10-31",
69
+ "3.13": "2029-10-31",
70
+ "3.14": "2030-10-31"
71
+ },
72
+ "spdx_replacements": {
73
+ "GPL-3.0": "GPL-3.0-only (or GPL-3.0-or-later)",
74
+ "GPL-2.0": "GPL-2.0-only (or GPL-2.0-or-later)",
75
+ "LGPL-3.0": "LGPL-3.0-only (or LGPL-3.0-or-later)",
76
+ "LGPL-2.1": "LGPL-2.1-only (or LGPL-2.1-or-later)",
77
+ "AGPL-3.0": "AGPL-3.0-only (or AGPL-3.0-or-later)",
78
+ "BSD-2-Clause-FreeBSD": "BSD-2-Clause-Views",
79
+ "MIT LICENSE": "MIT",
80
+ "MIT LICENCE": "MIT",
81
+ "APACHE 2.0": "Apache-2.0",
82
+ "APACHE-2": "Apache-2.0",
83
+ "APACHE2.0": "Apache-2.0",
84
+ "APACHE LICENSE 2.0": "Apache-2.0",
85
+ "BSD": "BSD-3-Clause (pick the exact variant)",
86
+ "BSD LICENSE": "BSD-3-Clause (pick the exact variant)",
87
+ "GPLV3": "GPL-3.0-or-later",
88
+ "GPLV2": "GPL-2.0-or-later",
89
+ "MPL2.0": "MPL-2.0",
90
+ "PUBLIC DOMAIN": "CC0-1.0 or Unlicense"
91
+ },
92
+ "setuptools_spdx_min": "77.0.0",
93
+ "rules": [
94
+ {"id": "project_table_missing", "sev": "error", "title": "No [project] table",
95
+ "spec": "PEP 621", "fix": "Add a [project] table with name, version, requires-python and license."},
96
+ {"id": "build_system_missing", "sev": "error", "title": "No [build-system] requires",
97
+ "spec": "PEP 518", "fix": "Add [build-system] with requires = [\"setuptools\u003e=77\"] and build-backend."},
98
+ {"id": "requires_python_missing", "sev": "error", "title": "requires-python absent",
99
+ "spec": "PEP 621", "fix": "Set requires-python so pip refuses interpreters you do not support."},
100
+ {"id": "requires_python_eol", "sev": "error", "title": "requires-python floor is end-of-life",
101
+ "spec": "python/devguide release cycle", "fix": "Raise the floor to a version that still gets security fixes."},
102
+ {"id": "requires_python_eol_soon", "sev": "warn", "title": "requires-python floor goes end-of-life within a year",
103
+ "spec": "python/devguide release cycle", "fix": "Plan the floor bump before that date, not after a CVE."},
104
+ {"id": "requires_python_upper_cap", "sev": "warn", "title": "Upper cap on requires-python",
105
+ "spec": "PEP 440 / pip resolver", "fix": "Drop the upper cap; a cap makes resolvers skip your package on new Python."},
106
+ {"id": "classifier_python_eol", "sev": "warn", "title": "Classifier advertises an end-of-life Python",
107
+ "spec": "PyPI trove classifiers", "fix": "Remove the classifier for that version."},
108
+ {"id": "classifier_python_mismatch", "sev": "error", "title": "Classifier contradicts requires-python",
109
+ "spec": "PEP 621", "fix": "Keep classifiers and requires-python on the same set of versions."},
110
+ {"id": "license_table_deprecated", "sev": "error", "title": "license as a table is deprecated",
111
+ "spec": "PEP 639", "fix": "Use license = \"\u003cSPDX expression\u003e\" and license-files = [\"LICENSE\"]."},
112
+ {"id": "license_classifier_deprecated", "sev": "warn", "title": "License :: classifier is deprecated",
113
+ "spec": "PEP 639", "fix": "Delete the License :: classifier; the SPDX expression carries it now."},
114
+ {"id": "license_classifier_conflict", "sev": "error", "title": "SPDX expression and License :: classifier together",
115
+ "spec": "PEP 639", "fix": "Keep the SPDX expression, delete every License :: classifier."},
116
+ {"id": "license_files_invalid_pattern", "sev": "error", "title": "license-files pattern is not a relative glob",
117
+ "spec": "PEP 639", "fix": "license-files patterns must be relative and must not contain '..'."},
118
+ {"id": "license_expression_not_spdx", "sev": "error", "title": "license is not a valid SPDX identifier",
119
+ "spec": "PEP 639 / SPDX licence list", "fix": "Replace it with the current SPDX identifier."},
120
+ {"id": "spdx_operator_case", "sev": "error", "title": "SPDX operator is lower-case",
121
+ "spec": "PEP 639 / SPDX expression syntax", "fix": "SPDX operators are AND, OR, WITH in upper case."},
122
+ {"id": "setuptools_too_old_for_spdx", "sev": "error", "title": "setuptools pin cannot read the SPDX license field",
123
+ "spec": "setuptools 77.0.0", "fix": "Raise the build requirement to setuptools\u003e=77."},
124
+ {"id": "dynamic_version_conflict", "sev": "error", "title": "Field is both static and dynamic",
125
+ "spec": "PEP 621", "fix": "A field listed in dynamic must not also be set statically."},
126
+ {"id": "project_name_not_normalized", "sev": "warn", "title": "Distribution name is not in normalised form",
127
+ "spec": "PEP 503 / PEP 508", "fix": "Lower-case it and use hyphens so the name you publish is the name people type."},
128
+ {"id": "readme_missing", "sev": "warn", "title": "No readme",
129
+ "spec": "PEP 621", "fix": "Point readme at your README file so the PyPI page is not blank."},
130
+ {"id": "urls_missing", "sev": "info", "title": "No [project.urls]",
131
+ "spec": "PEP 621", "fix": "Add Homepage / Source / Issues so the PyPI sidebar has links."}
132
+ ]
133
+ }
134
+ ;</script>
135
+ <script>// pyproject.toml Release Gate — the brain. One file, runs in Node and in the browser.
136
+ 'use strict';
137
+
138
+ var DATA = (typeof module !== 'undefined' && module.exports) ? require('./rules.json') : window.PJ_RULES;
139
+ var RULES = DATA.rules;
140
+ var EOL = DATA.python_eol;
141
+ var SPDX_FIX = DATA.spdx_replacements;
142
+ var SETUPTOOLS_MIN = DATA.setuptools_spdx_min;
143
+
144
+ var SPDX_KNOWN = ['MIT', 'MIT-0', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'BSD-4-Clause', 'ISC', 'MPL-2.0',
145
+ 'GPL-2.0-only', 'GPL-2.0-or-later', 'GPL-3.0-only', 'GPL-3.0-or-later', 'LGPL-2.1-only', 'LGPL-2.1-or-later',
146
+ 'LGPL-3.0-only', 'LGPL-3.0-or-later', 'AGPL-3.0-only', 'AGPL-3.0-or-later', 'Unlicense', 'CC0-1.0', 'CC-BY-4.0',
147
+ 'CC-BY-SA-4.0', 'Zlib', 'PSF-2.0', 'Python-2.0', 'BSL-1.0', 'EPL-2.0', 'Artistic-2.0', 'PostgreSQL', 'OFL-1.1',
148
+ 'Apache-2.0-WITH-LLVM-exception', 'LicenseRef-Proprietary'];
149
+
150
+ function byId(id) { for (var i = 0; i < RULES.length; i++) if (RULES[i].id === id) return RULES[i]; return { id: id, sev: 'error', fix: '' }; }
151
+ function days(fromISO, toISO) { return Math.round((Date.parse(toISO + 'T00:00:00Z') - Date.parse(fromISO + 'T00:00:00Z')) / 86400000); }
152
+ function verKey(v) { var p = String(v).split('.'); return parseInt(p[0], 10) * 1000 + (parseInt(p[1], 10) || 0); }
153
+ function eolVersions() { var out = []; for (var k in EOL) if (EOL.hasOwnProperty(k)) out.push(k); out.sort(function (a, b) { return verKey(a) - verKey(b); }); return out; }
154
+
155
+ // Recommend the lowest listed Python that still has at least a year of upstream security support left.
156
+ function recommendFloor(today) {
157
+ var vs = eolVersions();
158
+ for (var i = 0; i < vs.length; i++) if (days(today, EOL[vs[i]]) >= 365) return vs[i];
159
+ return vs[vs.length - 1];
160
+ }
161
+
162
+ // A deliberately line-oriented reader: enough to see every [project] key, small enough to run in a browser tab.
163
+ function parse(text) {
164
+ var lines = String(text == null ? '' : text).split(/\r?\n/);
165
+ var tables = {}; // table name -> { key: {raw, line} }
166
+ var order = [];
167
+ var table = '';
168
+ var pending = null; // multi-line array/inline table being collected
169
+ for (var i = 0; i < lines.length; i++) {
170
+ var raw = lines[i];
171
+ var line = raw.replace(/(^|\s)#(?![^"']*["'][^"']*$).*$/, '').trim();
172
+ if (pending) {
173
+ pending.raw += ' ' + line;
174
+ if (balanced(pending.raw)) { store(pending.table, pending.key, pending.raw, pending.line); pending = null; }
175
+ continue;
176
+ }
177
+ if (!line) continue;
178
+ var th = line.match(/^\[\[?([^\]]+)\]\]?/);
179
+ if (th) { table = th[1].trim(); if (order.indexOf(table) < 0) order.push(table); if (!tables[table]) tables[table] = { __line: i + 1 }; continue; }
180
+ var kv = line.match(/^["']?([A-Za-z0-9_.-]+)["']?\s*=\s*(.*)$/);
181
+ if (!kv) continue;
182
+ if (!balanced(kv[2])) { pending = { table: table, key: kv[1], raw: kv[2], line: i + 1 }; continue; }
183
+ store(table, kv[1], kv[2], i + 1);
184
+ }
185
+ function store(t, k, v, ln) { if (!tables[t]) { tables[t] = { __line: ln }; if (order.indexOf(t) < 0) order.push(t); } tables[t][k] = { raw: String(v).trim(), line: ln }; }
186
+ function balanced(s) {
187
+ var d = 0, q = null;
188
+ for (var j = 0; j < s.length; j++) {
189
+ var c = s[j];
190
+ if (q) { if (c === q) q = null; continue; }
191
+ if (c === '"' || c === "'") { q = c; continue; }
192
+ if (c === '[' || c === '{') d++;
193
+ if (c === ']' || c === '}') d--;
194
+ }
195
+ return d <= 0 && !q;
196
+ }
197
+ return { tables: tables, order: order, lines: lines };
198
+ }
199
+
200
+ function get(doc, table, key) { var t = doc.tables[table]; return (t && t[key]) ? t[key] : null; }
201
+ function strval(cell) { if (!cell) return null; var m = cell.raw.match(/^["'](.*)["']\s*,?$/); return m ? m[1] : null; }
202
+ function arrval(cell) {
203
+ if (!cell) return [];
204
+ var out = [], re = /["']([^"']*)["']/g, m;
205
+ while ((m = re.exec(cell.raw))) out.push(m[1]);
206
+ return out;
207
+ }
208
+ function isTable(cell) { return !!cell && /^\{/.test(cell.raw); }
209
+
210
+ // ">=3.9,<4" -> {floor:"3.9", cap:"4"}
211
+ function pyBounds(spec) {
212
+ var floor = null, cap = null;
213
+ var re = /(>=|>|~=|==|<=|<)\s*([0-9]+(?:\.[0-9]+)?)/g, m;
214
+ while ((m = re.exec(spec))) {
215
+ var op = m[1], v = m[2];
216
+ if (op === '>=' || op === '~=' || op === '==') { if (!floor || verKey(v) < verKey(floor)) floor = v; }
217
+ else if (op === '>') { var bumped = bumpMinor(v); if (!floor || verKey(bumped) < verKey(floor)) floor = bumped; }
218
+ else if (op === '<' || op === '<=') { if (!cap || verKey(v) > verKey(cap)) cap = v; }
219
+ }
220
+ return { floor: floor, cap: cap, capOp: /<=?/.test(spec) ? (spec.indexOf('<=') >= 0 ? '<=' : '<') : null };
221
+ }
222
+ function bumpMinor(v) { var p = String(v).split('.'); return p[0] + '.' + ((parseInt(p[1], 10) || 0) + 1); }
223
+
224
+ function pinnedSetuptools(reqs) {
225
+ for (var i = 0; i < reqs.length; i++) {
226
+ var m = reqs[i].match(/^\s*setuptools\s*(?:\[[^\]]*\])?\s*(.*)$/i);
227
+ if (!m) continue;
228
+ var sp = m[1] || '';
229
+ var lo = sp.match(/>=\s*([0-9][0-9.]*)/);
230
+ var eq = sp.match(/==\s*([0-9][0-9.]*)/);
231
+ var cap = sp.match(/<\s*([0-9][0-9.]*)/);
232
+ return { spec: reqs[i].trim(), lo: lo ? lo[1] : (eq ? eq[1] : null), cap: cap ? cap[1] : null, any: !sp.trim() };
233
+ }
234
+ return null;
235
+ }
236
+ function major(v) { return parseInt(String(v).split('.')[0], 10) || 0; }
237
+
238
+ function check(text, opts) {
239
+ opts = opts || {};
240
+ var today = /^\d{4}-\d{2}-\d{2}$/.test(String(opts.today || '')) ? opts.today : new Date().toISOString().slice(0, 10);
241
+ var doc = parse(text);
242
+ var F = [];
243
+ function add(id, msg, line) { var r = byId(id); F.push({ check: id, sev: r.sev, msg: msg + (r.fix ? ' → ' + r.fix : ''), line: line || 1, spec: r.spec || '' }); }
244
+
245
+ var proj = doc.tables['project'];
246
+ if (!proj) {
247
+ add('project_table_missing', 'This pyproject.toml has no [project] table, so the build back end takes its metadata from somewhere else (setup.py, setup.cfg) and nothing here is authoritative.', 1);
248
+ return { findings: F, today: today, rule_count: RULES.length };
249
+ }
250
+
251
+ // --- build-system -------------------------------------------------------
252
+ var bs = doc.tables['build-system'];
253
+ var bsReq = get(doc, 'build-system', 'requires');
254
+ if (!bs || !bsReq) add('build_system_missing', 'No [build-system] requires, so pip guesses a legacy setuptools build and your wheel metadata depends on whatever setuptools the machine happens to have.', bs ? bs.__line : 1);
255
+
256
+ // --- requires-python ----------------------------------------------------
257
+ var rpCell = get(doc, 'project', 'requires-python');
258
+ var rp = strval(rpCell);
259
+ var floorRec = recommendFloor(today);
260
+ var bounds = rp ? pyBounds(rp) : { floor: null, cap: null };
261
+ if (!rp) {
262
+ add('requires_python_missing', 'No requires-python, so pip will install this on interpreters you have never tested, including ones past end of life.', proj.__line);
263
+ } else {
264
+ if (bounds.floor && EOL[bounds.floor]) {
265
+ var d = days(EOL[bounds.floor], today);
266
+ if (d > 0) add('requires_python_eol', 'requires-python = "' + rp + '" still promises Python ' + bounds.floor + ', whose upstream security support ended ' + EOL[bounds.floor] + ' — ' + d + ' days before ' + today + '. The lowest floor with a year of support left is ' + floorRec + '.', rpCell.line);
267
+ else if (-d <= 365) add('requires_python_eol_soon', 'requires-python = "' + rp + '" floors at Python ' + bounds.floor + ', which reaches end of life ' + EOL[bounds.floor] + ' — ' + (-d) + ' days after ' + today + '.', rpCell.line);
268
+ }
269
+ if (bounds.cap) add('requires_python_upper_cap', 'requires-python = "' + rp + '" caps the interpreter at ' + bounds.cap + '. On the next Python release, resolvers will silently pick an older version of your package, or none.', rpCell.line);
270
+ }
271
+
272
+ // --- classifiers --------------------------------------------------------
273
+ var clsCell = get(doc, 'project', 'classifiers');
274
+ var cls = arrval(clsCell);
275
+ var licClassifiers = [];
276
+ for (var i = 0; i < cls.length; i++) {
277
+ var c = cls[i];
278
+ var pv = c.match(/^Programming Language :: Python :: ([0-9]+\.[0-9]+)$/);
279
+ if (pv) {
280
+ var v = pv[1];
281
+ if (EOL[v] && days(EOL[v], today) > 0) add('classifier_python_eol', 'Classifier "' + c + '" tells PyPI you support Python ' + v + ', which went end of life ' + EOL[v] + '.', clsCell.line);
282
+ if (bounds.floor && verKey(v) < verKey(bounds.floor)) add('classifier_python_mismatch', 'Classifier "' + c + '" advertises Python ' + v + ' but requires-python = "' + rp + '" refuses it, so pip and your PyPI page disagree.', clsCell.line);
283
+ }
284
+ if (/^License :: /.test(c)) licClassifiers.push(c);
285
+ }
286
+ if (!clsCell) { /* classifiers are optional; no rule fires */ }
287
+
288
+ // --- licence (PEP 639) ---------------------------------------------------
289
+ var licCell = get(doc, 'project', 'license');
290
+ var licStr = strval(licCell);
291
+ var lfCell = get(doc, 'project', 'license-files');
292
+ if (isTable(licCell)) {
293
+ var kind = /file\s*=/.test(licCell.raw) ? 'file' : 'text';
294
+ add('license_table_deprecated', 'license = { ' + kind + ' = ... } is the pre-PEP-639 form. setuptools ' + SETUPTOOLS_MIN + ' and later deprecate it, and the wheel it produces carries no SPDX expression for tools to read.', licCell.line);
295
+ }
296
+ if (licClassifiers.length) {
297
+ if (licStr) add('license_classifier_conflict', 'Both license = "' + licStr + '" and ' + licClassifiers.length + ' License :: classifier' + (licClassifiers.length === 1 ? '' : 's') + ' ("' + licClassifiers[0] + '") are present. Under PEP 639 setuptools refuses that combination and the build stops.', clsCell.line);
298
+ else add('license_classifier_deprecated', licClassifiers.length + ' License :: classifier' + (licClassifiers.length === 1 ? '' : 's') + ' ("' + licClassifiers[0] + '") carry the licence. PEP 639 deprecates them in favour of an SPDX expression.', clsCell.line);
299
+ }
300
+ if (licStr) {
301
+ var expr = licStr.trim();
302
+ if (/(^|\s)(and|or|with)(\s|$)/.test(expr)) add('spdx_operator_case', 'license = "' + expr + '" uses a lower-case operator. SPDX expressions only accept AND, OR and WITH in upper case, so this expression does not parse.', licCell.line);
303
+ var atoms = expr.replace(/[()]/g, ' ').split(/\s+(?:AND|OR|WITH|and|or|with)\s+/);
304
+ for (var a = 0; a < atoms.length; a++) {
305
+ var at = atoms[a].trim().replace(/\+$/, '');
306
+ if (!at) continue;
307
+ var fixKey = SPDX_FIX[at] ? at : (SPDX_FIX[at.toUpperCase()] ? at.toUpperCase() : null);
308
+ if (fixKey) add('license_expression_not_spdx', 'license = "' + expr + '" uses "' + at + '", which is not a current SPDX identifier. Use ' + SPDX_FIX[fixKey] + '.', licCell.line);
309
+ else if (SPDX_KNOWN.indexOf(at) < 0 && !/^LicenseRef-[A-Za-z0-9.-]+$/.test(at)) add('license_expression_not_spdx', 'license = "' + expr + '" contains "' + at + '", which is not on the SPDX licence list, so PyPI cannot validate the expression.', licCell.line);
310
+ }
311
+ }
312
+ var lf = arrval(lfCell);
313
+ for (var k = 0; k < lf.length; k++) {
314
+ if (/^[\\/]/.test(lf[k]) || lf[k].indexOf('..') >= 0 || /^[A-Za-z]:/.test(lf[k])) add('license_files_invalid_pattern', 'license-files entry "' + lf[k] + '" is not a relative glob inside the project. PEP 639 rejects absolute paths and parent traversal.', lfCell.line);
315
+ }
316
+ if (licStr && bsReq) {
317
+ var st = pinnedSetuptools(arrval(bsReq));
318
+ if (st && (st.any || (st.lo && major(st.lo) < major(SETUPTOOLS_MIN)) || (st.cap && major(st.cap) <= major(SETUPTOOLS_MIN)))) {
319
+ add('setuptools_too_old_for_spdx', 'license = "' + licStr + '" needs setuptools ' + SETUPTOOLS_MIN + '+, but [build-system] requires says "' + st.spec + '". A fresh build box can resolve an older setuptools that drops the field.', bsReq.line);
320
+ }
321
+ }
322
+
323
+ // --- dynamic vs static ---------------------------------------------------
324
+ var dynCell = get(doc, 'project', 'dynamic');
325
+ var dyn = arrval(dynCell);
326
+ for (var y = 0; y < dyn.length; y++) {
327
+ var f = dyn[y];
328
+ if (get(doc, 'project', f)) add('dynamic_version_conflict', '"' + f + '" is listed in dynamic and also set statically on line ' + get(doc, 'project', f).line + '. PEP 621 makes that a hard build error.', dynCell.line);
329
+ }
330
+
331
+ // --- name, readme, urls ---------------------------------------------------
332
+ var nameCell = get(doc, 'project', 'name');
333
+ var name = strval(nameCell);
334
+ if (name) {
335
+ var norm = name.toLowerCase().replace(/[-_.]+/g, '-');
336
+ if (norm !== name) add('project_name_not_normalized', 'name = "' + name + '" normalises to "' + norm + '" on PyPI, so the page, the install command and your docs can all disagree.', nameCell.line);
337
+ }
338
+ if (!get(doc, 'project', 'readme')) add('readme_missing', 'No readme, so the project page on PyPI opens with an empty description.', proj.__line);
339
+ if (!doc.tables['project.urls']) add('urls_missing', 'No [project.urls], so the PyPI sidebar has no Source, Issues or Changelog link.', proj.__line);
340
+
341
+ F.sort(function (p, q) { return (p.line || 0) - (q.line || 0); });
342
+ return { findings: F, today: today, rule_count: RULES.length };
343
+ }
344
+
345
+ var PJENGINE = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length, parse: parse };
346
+ if (typeof module !== 'undefined' && module.exports) module.exports = PJENGINE;
347
+ else window.PJENGINE = PJENGINE;
348
+ </script>
349
+ <script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
350
+ 'use strict';
351
+ function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
352
+ function summaryLine(res) {
353
+ const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length;
354
+ return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.';
355
+ }
356
+ function toText(res, label) {
357
+ const f = (res && res.findings) || [];
358
+ const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)];
359
+ f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); });
360
+ return lines.join('\n');
361
+ }
362
+ function toHtml(res) {
363
+ const f = (res && res.findings) || [];
364
+ if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>';
365
+ const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join('');
366
+ return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>';
367
+ }
368
+ var PJREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
369
+ if (typeof module !== 'undefined' && module.exports) module.exports = PJREPORT;
370
+ else window.PJREPORT = PJREPORT;
371
+ </script>
372
+ <script>
373
+ (function(){
374
+ var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today');
375
+ today.value=new Date().toISOString().slice(0,10);
376
+ function run(){ try { var res=window.PJENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.PJREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } }
377
+ go.onclick=run; if (src.value.trim()) run();
378
+ })();
379
+ </script>
380
+
381
+
382
+ <section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://marketplace.visualstudio.com/items?itemName=ReadyStack.pyproject-release-gate" rel="noopener" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">VS Code Marketplace</a><a href="https://open-vsx.org/extension/readystack/pyproject-release-gate" rel="noopener" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><a href="https://www.npmjs.com/package/@readystack/pyproject-release-gate" rel="noopener" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://www.npmjs.com/package/@readystack/pyproject-release-gate" rel="noopener" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">MCP server (npx)</a><a href="https://hub.docker.com/r/getreadystack/pyproject-release-gate" rel="noopener" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">ext install ReadyStack.pyproject-release-gate
383
+ npx @readystack/pyproject-release-gate &lt;file&gt;
384
+ npx @readystack/pyproject-release-gate --mcp</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://buy.polar.sh/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
385
+ <section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get one email when this rule changes</b><div style="opacity:.78;margin:.35rem 0 .8rem">We watch the regulation and vendor sources behind pyproject.toml Release Gate (PEP 639) every day. When a rule changes, you get a single email with what changed and the updated check. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Notify me</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">You're on the list. One email when it changes.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"pyproject-release-gate",lang:"en",source:"rule_alert"})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script></section>
386
+
387
+ <section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">19 date-aware checks on the [project] metadata that decides whether your next release uploads</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">One hour of a senior Python developer's time costs more than the licence, in any market that publishes to PyPI — and without this the review has to happen by hand at every release.</p><a href="https://buy.polar.sh/polar_cl_Dwv8rDHzgqaWnEsVKKqSDvaTApy7kMa9XNhx70ttYbL" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
388
+ <section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-11">Updated 2026-09-11</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-11","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku37307_result_card.jpg" alt="pyproject.toml Release Gate (PEP 639)" loading="lazy" style="max-width:100%;border-radius:6px"></p>
389
+ <p>Eight of the fourteen findings in one 26-line pyproject.toml are errors, and the maintainer who wrote it is a Python library author publishing wheels to PyPI from GitHub Actions. Nothing in it is a typo. Every line was correct when it was written.</p>
390
+ <p>Packaging metadata goes stale on a calendar, and neither your tests nor your build tools read a calendar.</p>
391
+ <p>The manifest, abbreviated:</p>
392
+ <p>[build-system] requires = [&quot;setuptools&gt;=61.0&quot;, &quot;wheel&quot;]</p>
393
+ <p>[project] version = &quot;1.4.0&quot; requires-python = &quot;&gt;=3.8,&lt;4&quot; license = &quot;Apache 2.0&quot; license-files = [&quot;/usr/share/licenses/invoice-toolkit/LICENSE&quot;, &quot;../COPYING&quot;] dynamic = [&quot;version&quot;] classifiers = [ &quot;Programming Language :: Python :: 3.7&quot;, &quot;License :: OSI Approved :: Apache Software License&quot;, ]</p>
394
+ <p>Run it through the 19 checks in pyproject.toml Release Gate with today set to 2026-09-11 and you get fourteen findings: eight errors, five warnings, one note.</p>
395
+ <p>The licence block accounts for five. PEP 639 replaced the old <code>license = { text = &quot;MIT&quot; }</code> table and the <code>License :: OSI Approved :: ...</code> classifiers with one SPDX expression plus a <code>license-files</code> list, and setuptools implements that from 77.0.0. So <code>license = &quot;Apache 2.0&quot;</code> is not an SPDX identifier — the identifier is <code>Apache-2.0</code>. The <code>License ::</code> classifier sitting next to an SPDX expression is not a warning; that combination is refused and the build stops. Both <code>license-files</code> entries are rejected too, because PEP 639 accepts only relative globs inside the project, so an absolute path and a <code>../</code> escape are out. And <code>setuptools&gt;=61.0</code> is loose enough that a fresh build box can resolve a setuptools that does not know the <code>license</code> field exists — which is how one repo builds correctly on a laptop and wrong in CI.</p>
396
+ <p>Then the interpreter. <code>requires-python = &quot;&gt;=3.8,&lt;4&quot;</code> promises Python 3.8, whose upstream security support ended 2024-10-07 — 704 days before the date above. The <code>Python :: 3.7</code> classifier advertises a version the same file&#x27;s <code>requires-python</code> refuses, so pip and your project page disagree in public. The <code>&lt;4</code> cap is its own failure: on the next Python release, resolvers skip your package.</p>
397
+ <p>The last error is structural. <code>version = &quot;1.4.0&quot;</code> is static while <code>version</code> is also listed in <code>dynamic</code>. PEP 621 makes that a hard build error, not a preference.</p>
398
+ <p>The surprise: <code>python -m build</code> and <code>twine check</code> pass a manifest whose interpreter floor died two years ago, because staleness is not a syntax error. Asking an assistant is worse, because it answers from pre-PEP-639 documentation and has no reliable idea what today&#x27;s date is — it hands back the same licence table and the same <code>&gt;=3.8</code> floor that caused the problem.</p>
399
+ <p>The date matters more than it looks. Take an otherwise clean manifest floored at <code>requires-python = &quot;&gt;=3.10&quot;</code>. On 2026-09-11 that is one warning: Python 3.10 reaches end of life 2026-10-31, fifty days out, so you bump the floor on your own schedule. On 2026-11-15 the same unchanged file is one error. The file did not move. The calendar did.</p>
400
+ <p>Finding out late has a price: a version number on PyPI cannot be re-used, so a bad release costs you a version, a changelog entry and an explanation.</p>
401
+ <p>The free tier finishes one job completely: all 19 checks on the file open in your editor, with the line, the spec that moved and the replacement text, plus the same engine as a browser page that uploads nothing. The licence changes scope and ownership rather than depth: every pyproject.toml in a monorepo in one pass, written to a dated report file that stays in the repo and goes into the pull request.</p></div><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does pyproject.toml Release Gate actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads the [project] and [build-system] tables of a pyproject.toml and reports 19 kinds of metadata that break a release: an interpreter floor past end of life, PEP 639 licence drift, classifiers that contradict requires-python, a setuptools pin too old to read the SPDX license field, and fields that are both static and dynamic. Each finding carries the line, the spec and the replacement text.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is this for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Python library maintainers who publish wheels to PyPI from GitHub Actions, and platform engineers who own a monorepo with dozens of pyproject.toml files written by different people in different years. If you tag releases and a failed upload costs you a version number, the checks apply to you.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why is a free tool or a chatbot not enough?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">twine check, build and pip all accept a manifest whose interpreter floor died two years ago, because staleness is a calendar fact rather than a syntax error. A chatbot answers from pre-PEP-639 training data and has no reliable sense of today&#x27;s date, so it returns the same license table and requires-python floor that caused the failure.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what needs a licence?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free covers one file completely: all 19 checks on the pyproject.toml open in your editor, with line numbers and replacement text, plus the same engine as a browser page. The licence changes scope and ownership, not depth — every manifest in the workspace in one pass, written to a dated report file you keep in the repo.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What would this cost without the extension?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">You would read PEP 621, PEP 639 and the CPython release cycle yourself, once per release, for every manifest. One hour of a senior Python developer&#x27;s time costs more than the licence in any market that publishes to PyPI, and a re-used version number cannot be re-uploaded once a bad wheel lands.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does pyproject.toml Release Gate actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads the [project] and [build-system] tables of a pyproject.toml and reports 19 kinds of metadata that break a release: an interpreter floor past end of life, PEP 639 licence drift, classifiers that contradict requires-python, a setuptools pin too old to read the SPDX license field, and fields that are both static and dynamic. Each finding carries the line, the spec and the replacement text."}}, {"@type": "Question", "name": "Who is this for?", "acceptedAnswer": {"@type": "Answer", "text": "Python library maintainers who publish wheels to PyPI from GitHub Actions, and platform engineers who own a monorepo with dozens of pyproject.toml files written by different people in different years. If you tag releases and a failed upload costs you a version number, the checks apply to you."}}, {"@type": "Question", "name": "Why is a free tool or a chatbot not enough?", "acceptedAnswer": {"@type": "Answer", "text": "twine check, build and pip all accept a manifest whose interpreter floor died two years ago, because staleness is a calendar fact rather than a syntax error. A chatbot answers from pre-PEP-639 training data and has no reliable sense of today's date, so it returns the same license table and requires-python floor that caused the failure."}}, {"@type": "Question", "name": "What is free and what needs a licence?", "acceptedAnswer": {"@type": "Answer", "text": "Free covers one file completely: all 19 checks on the pyproject.toml open in your editor, with line numbers and replacement text, plus the same engine as a browser page. The licence changes scope and ownership, not depth — every manifest in the workspace in one pass, written to a dated report file you keep in the repo."}}, {"@type": "Question", "name": "What would this cost without the extension?", "acceptedAnswer": {"@type": "Answer", "text": "You would read PEP 621, PEP 639 and the CPython release cycle yourself, once per release, for every manifest. One hour of a senior Python developer's time costs more than the licence in any market that publishes to PyPI, and a re-used version number cannot be re-uploaded once a bad wheel lands."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:37307,slug:"pyproject-release-gate",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence, 7-day refund.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/autohotkey-toml-devicetree-snippets"><span class="yj-rel-t">AutoHotkey v2, TOML and DeviceTree checks for VS Code</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/firmware-release-gate"><span class="yj-rel-t">Firmware Release Gate for sdkconfig and prj.conf</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/spdx-license-field-lint"><span class="yj-rel-t">SPDX License Field Lint for package.json, pyproject.toml and Cargo.toml</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/project-dashboard-template-tableau"><span class="yj-rel-t">Project Dashboard Template for Tableau — PMO Schedule, Budget &amp; Risk (.twb, co</span><span class="yj-rel-p">$120</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/play-release-blocker-lint"><span class="yj-rel-t">Google Play Release Blocker Lint - target API 36, Billing 8, 16 KB</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/linker-map-auditor"><span class="yj-rel-t">Linker Map Auditor - FLASH &amp; RAM budget for GNU ld map files</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section><!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div>
402
+ <script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||'')})}).catch(function(){});}catch(e){}})();</script>
403
+
404
+ <script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use'})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script>
405
+ </body>
406
  </html>