-
-
Notifications
You must be signed in to change notification settings - Fork 29
1178 lines (1142 loc) · 54.4 KB
/
Copy pathpython-app.yml
File metadata and controls
1178 lines (1142 loc) · 54.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
name: Python CI
on:
push:
branches: [ "master", "main" ]
# No base-branch filter, deliberately. A pull request that targets another
# branch is the bottom half of a stack, and it is the half that still has to
# be reviewed and merged: with `branches` set to main it arrived with four
# checks instead of the thirty this workflow runs, and the gates only spoke
# once it was already at the front of the queue.
#
# This does not double any run. The `push` trigger above is limited to the
# two trunk branches, so a commit on a working branch fires `pull_request`
# alone and a commit on main fires `push` alone. The two jobs that care which
# one it was, `sonar` and `pr-comment`, key on `github.event_name` rather
# than on the base, so they behave for a stacked pull request exactly as they
# do for any other.
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
quality:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
# A conflict resolved carelessly during a rebase can leave markers behind in
# a file no other gate reads. CHANGELOG.md is the usual victim: it collides
# on every parallel branch and nothing lints it, so the markers ride a fully
# green pull request all the way to main. Only the opening and closing
# markers are matched, never a bare row of "=", because that is legitimate
# Markdown (a setext heading underline).
- name: Check for merge-conflict markers
run: |
if git grep -nE '^(<{7} |>{7} )' -- . ; then
echo "::error::Unresolved merge-conflict markers found in the files listed above."
exit 1
fi
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
pip install -e .
# The clips are the one committed asset CI never regenerates: an FDTD run
# plus four video encodes each is far too slow for a pull request, and the
# encoders are not bit-reproducible anyway. So nothing could tell that the
# code drawing a clip had moved since the clip was written, and twelve of
# them shipped an ASCII hyphen in their Spanish tick labels for months
# after that was repaired, with every gate green. Each render stamps a
# fingerprint of the code that drew it; this recomputes the fingerprints
# from the sources and names any clip whose one has moved. It parses the
# figure package rather than importing it, so it needs no rendering stack
# and runs in seconds. The clips themselves live in
# jmrplens/phonometry-assets, so "is every file there" is answered from
# the tree of the commit assets.lock records: a blobless fetch of that one
# commit is a few hundred kilobytes, where the clips are a third of a
# gigabyte, and the check only ever needed the names.
- name: Fetch the names of the published clips, without the clips
run: |
git init -q assets-tree
git -C assets-tree remote add origin https://github.com/jmrplens/phonometry-assets.git
git -C assets-tree fetch -q --depth 1 --filter=blob:none origin "$(cat assets.lock)"
git -C assets-tree ls-tree -r --name-only FETCH_HEAD images/ > assets-manifest.txt
echo "$(wc -l < assets-manifest.txt) clip files published at $(cut -c1-12 assets.lock)"
- name: Check every published clip matches the code that draws it
run: python scripts/check_animation_freshness.py --manifest assets-manifest.txt
- name: Lint with Ruff
run: ruff check .
# Ruff is also the formatter. Before this step the tree had no formatter at
# all and 673 of its 812 Python files disagreed with any single style; the
# gate is what keeps that from happening again. Markdown is excluded in
# `[tool.ruff]`, so this never touches the documentation snippets.
- name: Check formatting with Ruff
run: ruff format --check .
# `stub/src` is in this list because it was in no path list at all. The
# PyOctaveBand transition shim lives outside the package this repository
# installs, so nothing here ever imported or type checked it, and two
# defects reached its published page that way. Ruff walks the tree from
# `.` and has always reached the shim; mypy takes the paths it is given,
# and this one was never among them. `tests/static_typing` holds what
# only a type checker can see, such as a published catalogue joined with
# a caller's being accepted by a lookup's `catalogue=`: the suite runs
# those files, but a wrong annotation passes at run time.
- name: Type check with Mypy
run: mypy src scripts stub/src tests/static_typing
- name: Security check with Bandit
run: bandit -r src
# The PyOctaveBand transition stub under stub/ is built from the tree by a
# workflow_dispatch publish job, so until now the first time anything looked
# at it was the moment it went to PyPI: a README that named the wrong
# warning class and a dependency line with no ceiling were both discovered
# after they had shipped. Building it on every pull request costs seconds
# and moves `twine check` (which is what renders the long description and
# would reject a malformed one) to where a mistake is still cheap.
transition-stub:
name: Transition stub builds and its metadata is sound
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
- name: Build the stub and check its long description
run: |
python -m pip install --upgrade pip build twine
python -m build stub/
twine check stub/dist/*
# The resolver reads the built metadata, not stub/pyproject.toml, so the
# cap that keeps `pip install -U PyOctaveBand` on the line whose API the
# shim re-exports is asserted where it will actually be enforced.
- name: Fail if the built metadata does not cap phonometry below the next major
run: python scripts/check_stub_metadata.py
# Fast gate (runtime deps only, no pytest): the committed
# docs/conformance.json, the docs/CONFORMANCE.md rendered from it, and every
# count quoted from either must equal a fresh `make conformance` run.
conformance:
name: Conformance report up to date
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: 'pip'
- name: Install runtime dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e .
- name: Regenerate the conformance artefact, the report and the quoted counts
run: make conformance
# `make conformance` now writes as well as reads: it regenerates the report
# and rewrites the counts quoted in .zenodo.json, the docs/ mirror and the
# site frontmatter. So the staleness gate has to cover the whole tree, not
# just the report. Diffing only docs/CONFORMANCE.md would let the rewrite
# fix the prose in the runner's checkout and pass, while the branch keeps
# the stale numbers.
- name: Fail if the committed report or any quoted count is stale
run: |
# --porcelain rather than `git diff`, because a diff is blind to an
# untracked file and the artefact is one of the things this regenerates.
# docs/conformance.json arrived untracked once and the job went green
# while a fresh checkout could not build the site at all.
if [ -n "$(git status --porcelain)" ]; then
git status --porcelain
git diff
echo "::error::The conformance artefact, the report or a count quoted from either is out of date, or a generated file is untracked - run 'make conformance' and commit the result."
exit 1
fi
# Belt and braces, and the command to run locally: the read-only check on
# the committed state. The report can be current while the prose quoting it
# is not; the counts were typed out in three places and two of them fell a
# release behind.
- name: Fail if any page quotes a stale conformance count
run: python scripts/check_conformance_claims.py
# The artefact is committed, so it can be wrong in ways a regeneration
# diff cannot see: a count that no longer matches its rows, a numpy scalar
# where a float belongs, a citation whose split no longer rebuilds it, a
# dead line in the override ratchet. This reads what is committed and says
# which one.
- name: Fail if the committed artefact is not internally consistent
run: python scripts/check_conformance_artifact.py
# The artefact's closed vocabularies are restated by the site: once in the
# Zod schema that validates it into the content collection, once in the
# label maps that word a verdict and a relation in each language. Nothing
# compared them until this, and a relation added to the reader and missing
# from the schema stopped the site build with a content error naming ten
# checks and no cause. Cheap and stdlib-only, so it runs here rather than
# waiting for the docs workflow to build the site.
- name: Fail if the site does not accept and word every vocabulary
run: python scripts/check_conformance_vocabulary.py
# Drift gate for the generated Starlight API reference: the committed pages
# under site/src/content/docs/reference/api and the sidebar fragment must
# equal a fresh `make api-docs` run (deterministic, stdlib-only generator).
api-docs:
name: Generated API reference up to date
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: 'pip'
- name: Install runtime dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e .
- name: Regenerate the API reference
run: make api-docs
- name: Fail if the committed API reference is stale
run: |
if ! git diff --exit-code -- site/src/content/docs/reference/api site/src/generated; then
echo "::error::The generated API reference is out of date - run 'make api-docs' and commit the result."
exit 1
fi
untracked=$(git ls-files --others --exclude-standard -- site/src/content/docs/reference/api site/src/generated)
if [ -n "$untracked" ]; then
echo "::error::Generated API pages are not committed:"
echo "$untracked"
exit 1
fi
# Drift gate for the llms artifacts: the committed llms.txt, llms-full.txt,
# the shards under site/public/llms and the generated overview mirrors under
# docs/ must equal a fresh `make llms` run. Every other generated artifact
# had this gate; these were regenerated by the docs workflow and never
# compared, so a stale commit could not be told from a fresh one.
llms:
name: llms artifacts up to date
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
- name: Regenerate the llms artifacts
run: make llms
- name: Fail if the committed artifacts are stale
run: |
if ! git diff --exit-code -- llms.txt llms-full.txt site/public/llms docs; then
echo "::error::The llms artifacts are out of date - run 'make llms' and commit the result."
exit 1
fi
untracked=$(git ls-files --others --exclude-standard -- site/public/llms docs)
if [ -n "$untracked" ]; then
echo "::error::Generated llms files are not committed:"
echo "$untracked"
exit 1
fi
# Drift gate for the evidence pages the site renders from docs/: the body of
# docs/ERRATA.md is transplanted into the two reference/errata pages (EN +
# ES), so the site can never show a hand-copied or stale version of it. The
# conformance pages are no longer transplanted - they render from
# docs/conformance.json through src/components/Conformance.astro, which the
# `conformance` job gates instead.
site-reports:
name: Generated evidence pages up to date
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
# Stdlib-only generator: no project install needed, it only reshapes text.
- name: Check the transplanted errata pages
run: python scripts/generate_site_reports.py --check
# Evidence gate for docs/ERRATA.md. Each entry there is a permanent public
# statement that a named body or author printed something wrong, so it must
# cite the page it quotes rather than an extraction: PDF text layers delete
# radicals and minus signs silently, and one entry was drafted on an
# extraction that had eaten a square root, and caught in review. The script
# flags any multiplicative claim whose ratio sits on sqrt(2), sqrt(3), pi,
# 2 pi, 1/sqrt(2), ln 2 or a small integer, which is the signature of a lost
# glyph. See scripts/check_errata_evidence.py and CONTRIBUTING.md.
errata-evidence:
name: Errata entries cite their page
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
# Stdlib-only checker: it reads docs/ERRATA.md and nothing else.
- name: Check the errata evidence
run: python scripts/check_errata_evidence.py
# The Python fences of a page form one sequential example: a later fence may
# use names an earlier fence defined, never the reverse. One shipped page
# used names its own figure block defined further down, while a same-named
# variable from a different room sat in scope, so reading top to bottom gave
# numbers that were not the annotated ones. Reader-owned placeholders (the
# reader's measurement, never invented by the page) are registered in the
# script. See scripts/check_fence_names.py and CONTRIBUTING.md.
fence-names:
name: Documentation fences read in order
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
# Stdlib-only checker: it parses the fences and imports nothing else.
- name: Check the fence reading order
run: python scripts/check_fence_names.py
# Coverage gate for the curated quick table in docs/reference/api/index.md: every
# phonometry.__all__ name must have a table row (extra rows such as methods
# or namespace entries are fine). See scripts/check_api_reference.py.
api-reference-table:
name: Curated API quick table covers __all__
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: 'pip'
- name: Install runtime dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e .
- name: Check docs/reference/api/index.md coverage
run: python scripts/check_api_reference.py
# A pressure of 101 325 and a pressure of 101.325 are both legitimate values
# here: the ISO 3740 family prints kilopascals, IEC 61094-2 and ISO 15186-3
# print pascals, and the result of the mix-up is still a float. No guard by
# magnitude can separate them, because either reading is a real duty
# somewhere else in the tree, so the unit lives in the parameter name and
# this holds it there. It walks the imported package rather than the source
# tree, because ReportMetadata is published from the root and defined in a
# private module, and a scan by file path does not see it.
parameter-units:
name: Every public quantity names its unit
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: 'pip'
- name: Install runtime dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e .
- name: Check that no pressure, temperature or humidity hides its unit
run: python scripts/check_parameter_units.py
# A published table is shared by every caller in the process, so a dict
# behind a `Mapping` annotation, or a writeable array, lets one caller change
# a printed number for everyone after it without anything raising. This holds
# every value reachable from a public name to immutable containers all the
# way down. It walks the imported package, like parameter-units, because a
# table built by a function call only shows its type once it exists.
frozen-constants:
name: Every published table refuses writes
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: 'pip'
- name: Install runtime dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e .
- name: Check that no published constant can be changed in place
run: python scripts/check_frozen_constants.py
# A value transcribed from a book is only checkable if the next reader can
# open the same page. The convention lived in review until a table came to
# cite a folio its page does not print and another attributed to a table two
# columns it does not carry. This holds every published record that carries a
# `source` to the grammar the errata registry uses, and every table
# transcribed from a book or a paper to a banner naming both its PDF page and
# its printed folio; a standard stays outside, because it numbers its own
# clauses and tables and is cited by that number. It walks the imported
# package, like the units guard, and re-runs its own census over the banners
# so a table from a new book fails until it is registered.
published-sources:
name: Every published value names its page
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: 'pip'
- name: Install runtime dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e .
- name: Check that every published value cites its page and folio
run: python scripts/check_published_sources.py
- name: Check that every published catalogue reaches the catalogues page
run: python scripts/check_published_catalogues_reach_the_page.py
# The site renders the catalogues from a module this dumps, so a row added
# to a data file without regenerating it would leave the page showing the
# catalogue as it was. Same arrangement as the API reference: the artefact
# is committed and CI checks it against a fresh run.
- name: Check the catalogue data the site renders
run: python scripts/generate_catalogue_data.py --check
# The JSON Schema of a catalogue file is written from the row classes, so
# a field added to one would be missing from every editor that completes
# a catalogue file until the published schema is written again.
- name: Check the catalogue schema the site publishes
run: python scripts/generate_catalogue_schema.py --check
# The solids catalogue's point is that a published value is worth more beside
# another published value than alone, and four books print the same material.
# Two densities that disagree are a digit somebody typed wrong, because every
# density the script does not already accept agrees across these tables to
# within 2,6 per cent, so it fails above 8 per cent and lets anything under
# that pass without a word. Moduli and speeds disagree by thirty per cent
# between books without anybody being wrong, so those it reports and never
# fails; the ones it reports by default are the few an order of magnitude
# apart, which is how tin's twelvefold modulus surfaced. It walks the
# imported package, like the units and sources guards.
solid-agreement:
name: The books agree about the solids they share
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: 'pip'
- name: Install runtime dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e .
- name: Check that the books agree about the solids they share
run: python scripts/check_solid_agreement.py
# The same idea over every catalogue: two books that credit one study for
# one material are two readings of one measurement, and a pair that does
# not overlap is a misread page rather than a disagreement.
- name: Check the rows two books took from the same place
run: python scripts/check_shared_sources.py
# The committed documentation figures (.github/images) must match a fresh
# `make graphs` run. The rendering + compute stack is pinned in
# requirements-figures.txt so SVG *structure* (elements, text, colours) and
# WebP dimensions/encoding are stable -- matplotlib/fonttools/pillow fix the
# layout, numpy/scipy fix the computed data. The pin is also load-bearing
# for the diagram plates' baked glyph outlines (scripts/diagrams/outline.py
# shapes them through the wheel's FT2Font/libraqm and DejaVu faces): a wheel
# bump rewrites every glyph in all 592 plate files and is reviewed as its
# own regeneration. The comparison itself (scripts/check_figures.py) is
# tolerance-aware rather than a byte diff: GitHub's heterogeneous runner
# CPUs shift a few path coordinates ~1 ULP, which is visually irrelevant but
# breaks a byte compare. Bump the pinned stack together with a fresh
# `make graphs` regeneration.
figures:
name: Documentation figures up to date
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: 'pip'
- name: Install the locked figure-rendering stack
run: |
python -m pip install --upgrade pip
pip install -e .
# Pin the render+compute stack last so it wins over looser deps.
pip install -r requirements-figures.txt
- name: Fail if any label's mathematics does not parse
# Before the regeneration, not after: an unparseable label aborts the
# generation, and this names the file and line instead of leaving a
# traceback in the middle of a four-hundred-figure run. Reads the
# sources, so it costs seconds and covers labels no figure exercises.
run: python scripts/check_mathtext.py
- name: Fail if a Spanish label would keep an English decimal point
# The one defect the language gate structurally cannot see: it compares
# untranslated WORDS, and a number is not a word. Static, so it needs no
# generation run.
run: python scripts/check_decimal_comma.py
- name: Regenerate figures and diagrams
run: make graphs
- name: Regenerate the plates again and demand identical bytes
# The diagram plates bake their labels to glyph outlines and must be
# bit-deterministic: two runs in the same job have to agree byte for
# byte, which is stronger than the tolerance gate below and catches an
# accidentally unordered collection in the glyph store the moment it
# appears.
run: |
mkdir /tmp/plates-first-run
cp .github/images/diagram_*.svg .github/images/sound_power_methods*.svg /tmp/plates-first-run/
python scripts/generate_diagrams.py
for f in /tmp/plates-first-run/*.svg; do
cmp "$f" ".github/images/$(basename "$f")"
done
- name: Fail if any committed figure is stale
# Tolerance-aware compare instead of a byte diff: GitHub's runner fleet is
# hardware-heterogeneous, so the pinned stack computes a few path
# coordinates ~1 ULP apart depending on the CPU the run lands on. That
# sub-pixel drift is irrelevant but breaks a byte diff; the script checks
# SVG structure + numeric tolerance and raster RMS instead, so real figure
# changes still fail while cross-CPU noise passes. See scripts/check_figures.py.
run: python scripts/check_figures.py
- name: Fail if any shaded region is illegible
# Staleness says the figures match the code; it says nothing about
# whether a shaded region can be seen. This measures every filled area
# against the page it is drawn on and fails below the CIEDE2000
# threshold, which is what catches a fill tuned on one theme and left
# invisible on the other. It parses the committed SVGs (a few seconds
# for the whole set) and needs no rendering stack of its own.
run: python scripts/check_figure_contrast.py
- name: Check every line can be seen on the dark page
# The step above measures filled areas. A stroke is not a fill, and the
# ISO 3745 microphone array shipped with its hemisphere drawn in a
# half-opacity grey hairline that composited to 1.3:1 on the dark theme:
# present in the SVG, invisible to a reader, and green on every gate.
# This blends each stroke with its own opacity over the dark page and
# holds hairlines to 3:1 and thicker lines to 2:1, with the grid and axis
# furniture exempt because they are drawn to recede.
run: python scripts/check_stroke_contrast.py
- name: Check the Spanish figures are in Spanish
# The Spanish variant of a figure is the English one with its strings
# looked up in a table at save time, so a string nobody added to the
# table ships in English inside `X_es.svg` while every other gate stays
# green: the page is Spanish, the i18n parity gate sees a translated
# page, this job sees a figure that matches its generator, and the
# figure is in English. That is how roughly two hundred of them reached
# the tree. The lookup itself writes down what it could not translate,
# during the `make graphs` above (no second render, and it cannot change
# a rendered byte), and this fails on any untranslated string the
# committed baseline does not already record -- and on a baseline line
# that has been paid off and not deleted.
run: python scripts/check_figure_language.py
- name: Fail if an annotation on a figure cannot be read
# Two ways a label fails the reader, and nothing else can see either. A
# label across a curve and given no backing is hard to read: the strokes
# of the letters and of the curve are the same weight, and on the dark
# page close to the same lightness. A label something is drawn over is
# not there at all. The figure matches its generator, the contrast steps
# above measure fills and strokes rather than what overlaps what, and
# `svg.fonttype = "path"` means the committed file has no text node to
# find the label in -- so the measurement has to happen while the
# matplotlib artists are alive, which is what the `make graphs` above
# did, for both language editions. This reads it and fails on any label
# over the calibrated number of pixels either way, printing the band
# below that for a person to judge.
run: python scripts/check_figure_annotations.py
- name: Fail if two tick labels of one axis run into each other
# A band axis set by hand on a logarithmic scale keeps matplotlib's own
# minor labels between the bands unless the minor formatter is cleared:
# the octave axis of enclosed_space_absorption shipped reading
# "2 × 10²50 4 × 10²500" under both panels. The figure matches its
# generator, the annotation step above measures the labels a generator
# places and not the ones an axis draws, and the committed file has no
# text node to find a tick in, so the `make graphs` above measures every
# axis of both language editions as it saves them. This fails on minor
# labels left to the scale beside major ticks set by hand, and on any
# two labels of one axis that touch.
run: python scripts/check_figure_ticks.py
- name: Fail if a Spanish figure numbers a tick with an English point
# The decimal-comma check above reads the label a generator writes; this
# reads the label a formatter wrote, in the figure as committed. They are
# different halves: three machines localise a Spanish figure (the
# library's localize_axes, format_frequency_axis where a caller hands it
# the language, and the save-time pass), and a panel none of them reached
# keeps its English point while the legend beside it carries the comma.
# A zoom inset, a contour colorbar, the z of a 3-D array and a log axis
# of distances each shipped that way. Reads the committed SVGs, so it
# needs no rendering stack.
run: python scripts/check_figure_decimal_point.py
- name: Fail if a legend closes over a plotted point
# A legend is an opaque plate the author places by hand, and its width is
# set by its longest label, which is a defect waiting to happen in a
# bilingual corpus: the Spanish label is routinely half again as long as
# the English one, so a box that clears the data in one language reaches
# back over it in the other. Every other gate stays green, because the
# figure matches its generator, the colours pass and the annotation audit
# measures labels rather than legends. This reads the committed SVGs for
# a plotted point under a legend frame, and carries the drawings where
# the mark under the box is a rail rather than a reading with the reason
# each one is a decision.
run: python scripts/check_figure_legends.py
- name: Fail if a legend, a curve or a marker covers a tick label
# A legend can land on a tick label as well as on a point, and a curve
# can run through one: the radial labels of a polar plot sit inside it,
# strung along one ray, and an inset writes its labels over the data of
# the panel it sits in. The tick step above measures the labels of one
# axis against each other and the annotation step the labels a
# generator places, so neither sees it. This reads the committed SVGs
# for anything drawn over a tick label, through the clip it is drawn
# with, so it needs no rendering stack.
run: python scripts/check_figure_tick_clearance.py
- name: Fail if a label is drawn over a tick label, another label or a line
# The step above measures the marks drawn over a tick label, not a
# text, and the annotation step leaves the plates out. A note placed by
# hand landed on the numbers of the axis beside it, the heading of a
# plate lane was crossed by the impulse it names, and a note that ran
# out of its axes was struck through by the spine. This reads the
# committed SVGs, figures and plates, glyph by glyph, so a label turned
# on a 3-D plate is read along its own baseline; the leader of an
# annotation, the grid and a line behind a chip pass.
run: python scripts/check_figure_text_clearance.py
- name: Fail if a figure signs a number with a hyphen
# The corpus signs its negative numbers with U+2212, and a polar angle
# formatter that ignores axes.unicode_minus, a reading built with an
# f-string and a plate that sets the characters it is handed each wrote
# the hyphen-minus instead, a shorter, lower glyph beside the proper one.
# This reads the string every committed SVG records beside its outlines,
# all four variants, figures and plates alike.
run: python scripts/check_figure_minus_sign.py
# The Python snippets printed in the guides must run. They are the first
# thing a reader copies, and nothing executed them until this job existed:
# the metrology split shipped seven blocks where `from phonometry import
# signals` sat next to `from scipy import signal` and silently rebound the
# name, which Python does not warn about and no other gate can see. The
# script also holds the English and Spanish pages to the same API and skips,
# with a written reason, the pages whose blocks are excerpts of a workflow
# rather than a script.
doc-snippets:
name: Documentation snippets run
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: 'pip'
- name: Install dependencies
# The guides plot and print fiches, so the snippets need the figure and
# report stacks as well as the package itself.
run: |
python -m pip install --upgrade pip
# The audio extra rides along because the io guide's snippets read and
# write the formats it serves; a guide that documents the extra has to
# run under it, the same reason the figure and report requirements are
# here for the pages that draw and render.
pip install -e ".[audio]"
pip install -r requirements-figures.txt -r requirements-reports.txt
- name: Run every snippet the guides print
run: python scripts/check_doc_snippets.py
# The guides are hard-wrapped, so a sentence can wrap onto a "-" or a ">" and
# stop being a sentence: CommonMark ends the paragraph at the marker. One
# variant takes the site build down (unclosed inline maths, which MDX then
# evaluates as JavaScript); the other is silent and ships a quoted block in
# the middle of a paragraph. Needs no dependencies, so it runs on its own
# rather than waiting behind an install.
markdown-hazards:
name: Markdown renders the way it reads
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
- name: Check the hand-written markdown
run: python scripts/check_markdown_hazards.py
- name: No em dash in the published prose
# The house style writes its prose without the em dash, in the pages,
# the mirrors, the CHANGELOG, the docstrings and the figure labels
# alike; a standard's title reproduced as printed keeps its own. Static,
# stdlib only, so it runs here with the markdown checks.
run: python scripts/check_em_dashes.py
- name: Every Spanish label keeps its accents and eñes
# A translation typed without the Spanish keyboard layout reads fine to
# every other gate, and twenty-nine figure labels shipped as
# "Correccion", "medicion" and "aqui". This reads the translation tables
# of the figures, the diagrams and the library's renderers, and the
# example fiches built in Spanish, and fails on a form that is never
# correct Spanish without its mark. Static, stdlib only, so it runs here
# with the other checks on published text.
run: python scripts/check_spanish_accents.py
# A form feed is one invisible byte, and inside a raw docstring it is the
# backslash and the f of the command that follows it: `\frac` was published
# as `rac`, with the fraction gone and no error anywhere. Guard the class
# rather than the byte. Needs no dependencies, so it runs on its own.
control-characters:
name: No stray control character
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
- name: Check the tracked text files
run: python scripts/check_control_characters.py
# ISO 80000-2 sets a subscript by what it is, and a glyph pair can honestly
# take both slopes: the z of the ISO 9613-2 barrier screening is a
# path-length difference, the z of the ISO 2631-5 dose is a direction, and
# both documents print D_z. So the slope is decided per file, which opens
# with the standard it implements, and two slopes for one symbol inside one
# file is a page contradicting itself. Static and dependency-free, so it runs
# on its own rather than waiting behind an install.
subscript-slope:
name: One meaning per subscript per file
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
- name: Check that no file sets one subscript both ways
run: python scripts/check_subscript_slope.py
# generate_api_docs copies the mathematics of a docstring to the site
# verbatim, and KaTeX reads what the docstring actually holds. A doubled
# backslash is a line break with a command after it, which KaTeX refuses,
# and the page still ships with everything after the bad block swallowed.
# The same four characters are correct in one kind of docstring and wrong in
# the other, so this reads the value Python builds rather than the file. It
# also refuses a bare command KaTeX will not take as a sub- or superscript
# (|dL_n|_\max where |dL_n|_{\max} is meant), in the docstrings and in the
# hand-written pages. The site job catches both too, after a full build; this
# costs a second and runs without one.
docstring-math:
name: Docstring mathematics reaches the page intact
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
- name: Check the docstrings and the pages for mathematics KaTeX refuses
run: python scripts/check_docstring_math.py
# Every helper that takes a language defaults to English, so a call that does
# not pass the caller's on raises nothing and ships an English string or an
# English decimal point in the Spanish figure. On a log frequency axis nothing
# in the library repairs it: localize_axes skips that axis, whose labels are
# fixed strings by the time it runs. Finding the calls needs the helper resolved
# through the imports, a result's .plot() through its receiver and a **kwargs
# through what it can carry, so the check is an AST walk rather than a grep.
# Static and dependency-free, so it runs on its own.
language-forwarding:
name: Every call passes the language on
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
- name: Check that no call leaves the caller's language behind
run: python scripts/check_language_forwarding.py
- name: Check that no style default collides with its own alias
run: python scripts/check_plot_style_defaults.py
- name: Check that no grouped number can break across a line
run: python scripts/check_digit_grouping.py
# A private module-level constant is a number lifted out of a standard so the
# code that uses it can say where it came from, and one nothing reads is
# either a leftover or the trace of a check the docstring above it still
# promises. The sweep this came from found both: a recommended specimen
# velocity carried and never checked, and a docstring promising a default the
# code did not take. Static and dependency-free, so it runs on its own.
dead-constants:
name: No private constant goes unread
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
- name: Check that every private constant is read somewhere
run: python scripts/check_dead_constants.py
# A level is a ratio to a reference value, and ISO 1683 fixes one per
# quantity. The package publishes them once, as
# phonometry.metrology.ISO1683_REFERENCE_VALUES; before that the same 20 µPa
# was typed out seventeen times and one name meant a pressure in four modules
# and a power in two. This fails on a reference value in src that repeats a
# value of the table instead of reading it, and on one that differs from it
# without naming the document it comes from. Deciding what a reference is
# needs the name, the value and the comment beside it, so it is an AST walk
# rather than a grep. Static and dependency-free, so it runs on its own.
reference-values:
name: Every reference value points at ISO 1683 or names its source
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
- name: Check that no reference value is a copy or has no document
run: python scripts/check_reference_values.py
# A conformance row states what a standard prints and what this library
# computes. A row that computes the expected value itself, from the same
# constants, compares a formula with a second copy of it and reports Pass
# whatever the library does; three did, and a wrong sign in the function they
# certified would have gone out green. Counting them needs the delegation
# resolved -- through helpers, across modules, and under whatever name the
# imports gave the library -- so the check is an AST walk rather than a grep.
# Static and dependency-free, so it runs on its own.
conformance-rows:
name: Every conformance row runs the library
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
- name: Check that no row certifies the library without running it
run: python scripts/check_conformance_rows.py
# The committed example .report() fiches (.github/reports) must match a fresh
# `make reports` run. Same drift gate as the figures, one layer further down
# the pipeline: the fiches are what the documentation links to as worked
# examples of what the library prints, so a stale one publishes a wrong
# answer. Nothing checked them until this job existed, and two of them sat a
# plot-styling release behind the code for weeks. The rendering stack is
# pinned across both requirements files (matplotlib/numpy in -figures,
# svglib/reportlab/pypdfium2 in -reports) because every link in the chain
# fixes the rendered page the check compares.
reports:
name: Example report fiches up to date
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: 'pip'
- name: Install the locked fiche-rendering stack
run: |
python -m pip install --upgrade pip
pip install -e .
# Pin the render+compute stack last so it wins over looser deps.
pip install -r requirements-figures.txt -r requirements-reports.txt
- name: Regenerate the example fiches
run: make reports
- name: Fail if any committed fiche is stale
# Tolerance-aware compare instead of a byte diff: GitHub's heterogeneous
# runner CPUs shift a few of the embedded plot's path coordinates ~1 ULP,
# which rewrites the PDF page stream for no visible reason. The script
# compares the extracted text exactly and the rendered page within a
# calibrated pixel tolerance, so a real fiche change fails while
# cross-CPU coordinate noise passes. See scripts/check_reports.py.
run: python scripts/check_reports.py
tests:
runs-on: ${{ matrix.os }}
permissions:
contents: read
# Required for the OIDC-based Codecov upload (no token secret needed).
id-token: write
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.13", "3.14"]
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
pip install -e .
# Heavy third-party oracle sets (the EBU loudness test set, the stipa.info
# certified STIPA bench, the NORAH2 public release) are never fetched here.
# The suites that use them fall back to the committed derived series and
# extracts under tests/data/ - see tests/data/README.md - which is the path
# CI takes, and whose assertions never skip. A handful of cases that exist
# only to exercise a full set do skip here and report as such (the EBU
# programme-audio cases and the full-bench inventory guard); pytest prints
# the resolution in its run header.
- name: Run tests
# -n auto fans the suite out across the runner's cores via pytest-xdist;
# pytest-cov combines the per-worker coverage into a single coverage.xml.
# Pin the numerical thread pools to one thread each so the per-core xdist
# workers do not oversubscribe the CPU with nested BLAS/OpenMP pools.
env:
OMP_NUM_THREADS: "1"
MKL_NUM_THREADS: "1"
OPENBLAS_NUM_THREADS: "1"
NUMEXPR_NUM_THREADS: "1"
VECLIB_MAXIMUM_THREADS: "1"
run: |
pytest -n auto --junitxml=test-results-${{ matrix.os }}-${{ matrix.python-version }}.xml --cov=src --cov-report=xml
- name: Upload Test Results
uses: actions/upload-artifact@v7