Repository navigation
6797 lines (6420 loc) · 394 KB
/
Copy pathlint.yml
File metadata and controls
6797 lines (6420 loc) · 394 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: Lint & Type Check
on:
push:
branches:
- main
# ⭐ NO `branches:` filter here, deliberately (#16482) — see ci.yml's `on:`
# block for the full rationale and the measured runner cost; both files carry
# required contexts and are widened together, because six contexts split
# across two files are only as reachable as the less reachable file.
#
# ⚠️ The MEASURED 2026-08-25 (#12211) block immediately below does NOT bind
# on this widening, and it is not touched by it. Read what it measured: it is
# a `merge_group` COVERAGE result — "is there a queue-build gap to close" —
# answered NO, plus a ⛔ against adding `merge_group` to the ADVISORY
# workflows. It says nothing about the `pull_request:` trigger's `branches:`
# filter, and its residual is explicitly named as two repository SETTINGS
# (a commit reaching main with no queue build; `strict_required_status_checks
# _policy: false`), "neither reachable from this file". Removing a base-branch
# filter from `pull_request:` changes no merge_group behaviour whatsoever:
# `merge_group:` is a separate trigger key, still present below, and
# `check:required-contexts` assertion 6 fails the build if it ever is not.
pull_request:
# Merge queue (see ci.yml for the full note): required checks must report on
# queue builds or the queue stalls. This workflow has no PR-only steps, so
# the trigger alone is enough.
merge_group:
# ── THE HOURLY FULL RUN (#16467), the half this file owns ────────────────
# The card's ruling is that the hourly run covers BOTH required-context
# files, not just ci.yml, and the reason is measured rather than symmetric:
# the push-on-`main` run of THIS workflow is the only post-merge full-battery
# run of the families `scripts/ci/select-gate-families.sh` scopes away on
# merge groups (the PM dispatch-gates self-test, both ratchets, the
# verify-lock self-test, the comment-mask corpus — and since #19498 the
# entry-guard sweep, the declared-population gate, the bare-root worklist
# self-test and the self-test workflow-command gate, whose whole-tree reads
# that ruling took off the PR path). A scoped family that goes red on `main`
# after a queue build skipped it had, until this trigger, no run that would
# notice and no filer that would say so.
#
# The selector already treats every event that is neither `merge_group` nor
# `pull_request` as "run every family", so this trigger alone restores the
# full battery here — no step in this file changes.
#
# ⛔ A NEW TRIGGER KEY, not a widening of `push:`; see ci.yml's `on:` block.
schedule:
- cron: '0 * * * *'
workflow_dispatch:
# ── MEASURED 2026-08-25 (#12211) — a NEGATIVE result, recorded so it is not
# re-measured. After the stale-ledger outage the queue's check set was measured
# rather than assumed, because two observations of that day read as
# contradictory: a PR landing an inconsistent tree at 11:50Z (which looked like
# "the consumer-gates lane does not run in the queue"), and five queue entries
# dequeued 12:26-13:05Z while main was red (which looked like the opposite).
#
# Both halves were read, and there is NO merge_group coverage gap to close:
#
# - STATIC. Every gate step in this workflow runs on merge_group; the claim
# three lines above is exact. The only event-conditioned step in the file is
# `Save Turbo cache (main only)`, which stores a cache and judges nothing.
# ci.yml is the same shape — its single merge_group exclusion sits on the
# paths filter, which on a queue build widens to "everything changed"
# rather than narrowing. All six required contexts live in these two
# merge_group-triggered workflows (scripts/check-required-contexts.mjs).
#
# - DYNAMIC. 180 merge_group runs of this workflow were read across
# 09:15-18:49Z. In run 32847794799 — a real queue build — the
# `Type Check · consumer gates` lane RAN and FAILED on
# `check:exported-any-returns`, and the required `TypeScript Type Check`
# aggregate carried that red into the queue. Tree-global ratchets do kick
# there; the 12:26-13:05Z dequeues are that gate, on innocent candidates,
# while main itself was red. CI was green on those same merge groups.
#
# What the 11:50Z landing actually was: that commit produced NO merge_group
# build at all — 4 of the 29 PRs landing 09:20-12:10Z produced none — and its
# PR-level required checks were green as measured at 07:20Z, ~3h before the
# gate that would have refused it existed. The residual is therefore not a
# trigger this file is missing. It is that a commit can reach main without a
# queue build, plus `strict_required_status_checks_policy: false` on the `main`
# ruleset (measured 2026-08-18, #9642). Both are repository SETTINGS and
# maintainer-only; neither is reachable from this file.
#
# ⛔ So do not "close the gap" by adding merge_group to the advisory workflows
# (check-links.yml, docs-drift-check.yml, validate-deps.yml, the patrols). A
# non-required check on a queue build costs runners and blocks nothing, and
# check-links.yml's own header already refuses it for that reason.
# Same policy as ci.yml: superseded runs on the same PR/branch waste runners
# and delay feedback; cancel them. Push runs to main group by commit ref, so an
# in-flight main run is cancelled only by a newer main push.
#
# ⭐ `github.event_name` is in the key for the reason ci.yml's concurrency block
# states in full (#16467): on a `schedule` event the remaining two segments are
# byte-identical to a push to `main`, so without it the next merge cancels the
# hourly full run. `scripts/ci/scheduled-full-run.mjs --check-concurrency`
# evaluates THIS expression too and reds if the two groups ever collapse again.
concurrency:
group: lint-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
# ─────────────────────────────────────────────────────────────────────────────
# ⭐ GATE INVOCATION IDIOM — stated ONCE, here. The steps below point at it.
#
# Most gate steps in this file run their script directly:
#
# run: node scripts/check-<thing>.mjs
#
# rather than through a `pnpm check:*` alias. That is a deliberate in-repo
# idiom with its own justification, not a workaround:
#
# * Many gate steps in this file already use it — it is the local precedent.
# * `scripts/pm/dispatch-gates.mjs` derives gate families from EITHER
# spelling, so the direct form loses no discovery.
# * It adds no key to the root manifest.
# * It is pinned rather than incidental: check-aggregator-roster,
# check-ci-filter-parity and check-doc-frontmatter each assert in their own
# self-test that `node <self>` appears in THIS file, so silently rewiring
# one of them to an alias reddens its gate.
#
# ⚠️ It is NOT because root `package.json` is off limits. This file used to say
# that, in one shape or another, at nineteen separate sites, and it is FALSE.
# The declared territory of the #9465 @changesets/cli v3 lane inside that file
# is scoped by the lane's own parenthetical, verbatim from the issue body:
#
# root `package.json` (the `@changesets/cli` range and the `version` script)
#
# That parenthetical is SCOPING, not illustrative: one dependency range and one
# script key. It does not cover the file, and it does not cover a new `check:*`
# key — so adding `check:<thing>` to the root manifest would have been allowed
# all along. The steps below take the direct form on the merits listed above;
# they are not fenced out of the alternative.
#
# Stated once here, and POINTED AT rather than restated, because the over-broad
# reading propagates by copying — measured, not supposed. A dev agent wrote the
# same sentence into a new comment and a new script's docblock and said plainly:
# "I copied the phrasing from neighbouring comments instead of checking it."
# The count was 15 sites when #10894 was filed on 2026-08-21 and 19 by
# 2026-08-24, four of them added AFTER the card was filed and two of those
# within ninety minutes of it being picked up. Nineteen CORRECTED copies would
# re-arm the identical mechanism: a claim that reads as verified because it
# reads exactly like the verified ones beside it. So there is one statement of
# this idiom — this one — and every step refers to it.
#
# Changing this note? It is prose, and no gate can red on a wrong sentence
# here; that is precisely how nineteen wrong ones accumulated. Check any claim
# about the #9465 fence against #9465's own body before you write it down.
# ─────────────────────────────────────────────────────────────────────────────
# ─────────────────────────────────────────────────────────────────────────────
# ⭐ EVERY GATE RUNS, AND ONE VERDICT STEP KEEPS THE RED (#23191; #23177
# ruling B, item A1, comment 6139629628). Every gate step of a `Lint · *` lane
# and of a `Type Check · *` lane carries `continue-on-error: true` and an
# `id:`, so a red gate no longer skips the gates behind it: one run reports
# every red in the lane, where it used to report the first and leave the rest
# for the next CI cycle. The steps the gates depend on — checkout, Node and
# pnpm setup, the caches, the install, the gate-family selector and the
# workspace builds — carry neither and stay fail-fast: a gate run over a
# missing install or a half-built tree would only add reds about the runner.
#
# `continue-on-error` alone trades a red for silence: the failed step's
# `conclusion` becomes `success` and so does the job's. What keeps the red is
# the LAST step of every lane, `Lane verdict` (`scripts/ci/lane-verdict.mjs`,
# whose header is the authority). It reads the lane's gate roster from this
# file — every step that sets `continue-on-error` — reads each gate's
# `outcome` from the `steps` context (the result BEFORE continue-on-error is
# applied; ⛔ never the `conclusion` after it), prints the red ones by name
# and exits 1. So a lane with a red gate still concludes `failure`, and the
# `Lint & Repo Gates` and `TypeScript Type Check` aggregates still go red.
# Adding a gate step: give it a unique `id:` and `continue-on-error: true`,
# like its neighbours. Without the `id:` the verdict cannot read it and reds
# the lane naming it; without the flag it is fail-fast again, and a red there
# hides every gate behind it once more — still red, one red per cycle.
#
# One gate feeds a later one: `check:authorable-surface` writes the gitignored
# json-schema tree that `check:docs` renders from (both in
# `typecheck-source-gates`). When the first cannot generate the tree, the
# second refuses on a missing or stale tree and says to generate it — a second
# red that names its cause, never a pass.
#
# The ORDER of the gates therefore no longer shortens a red run. The #13690
# order (maintainer ruling 2026-09-03: the cheap, frequently-red
# `Docs anchors resolve to real headings` and `ADR anchors + number
# uniqueness` first, because they produced 78% of the single job's reds) is
# kept as it stands, at the top of `lint-repo-tooling`, because moving steps
# buys nothing — but ⛔ no gate is placed anywhere to shorten a red run any
# more: the verdict step is what keeps the red. A green run's wall clock was
# always the sum of its steps; a red run's now is too.
# ─────────────────────────────────────────────────────────────────────────────
# ⭐ WHERE A GATE LIVES, stated once, so that a step whose note is a one-line
# `Rationale:` pointer to its script's header need not argue it again. No
# trigger or job in this file carries a paths filter, and the two required
# contexts aggregate every job here, so every job runs on every PR, queue build
# and `main` run: a gate placed in this file cannot go dormant on the PR that
# breaks it because a filter on one side of its inputs skipped the job. A step
# that narrows its own work on PR and queue runs (the gate-family selection, the
# affected-package type check) carries its own note saying how.
# ─────────────────────────────────────────────────────────────────────────────
jobs:
lint:
# ⚠️ This `name:` IS the required-status-check context in repository
# Settings → Rulesets (a job's check-run name is its `name:`), so it is
# pinned as contract by `scripts/check-required-contexts.mjs` and may only
# be changed together with that registry AND the Settings entry, in one
# maintainer-present sitting (#9325 ruling 2026-08-17). Either half alone
# is an outage: rename-first leaves the old context permanently pending,
# which wedges every open PR and the merge queue; settings-first drops the
# whole gate family to advisory with no signal anywhere, which is #5617
# verbatim.
#
# It is not called `ESLint` any more because it never was only that: the
# `pnpm lint` step (now in the `lint-eslint` lane) was one of ~70 sequential
# gate steps when this was written, so a red here was routinely read as "a
# lint problem" when it was a repo gate — three mis-routed diagnoses on
# 2026-08-17 alone (#9258, PRs #9256/#9291).
#
# ── AN AGGREGATE SINCE #22991 (ruling 6120820653, letter D) ──────────────
# This job used to run every gate itself: 193 serial steps on one runner,
# p50 17.8 / p90 35.9 / max 38.5 minutes over the 70 successful runs read
# for #22991 (2026-10-12T04:40Z → 08:45Z, `pull_request` and
# `merge_group`, job and step timestamps from the jobs API), and the job
# the merge queue waited on in 13 of 25 groups. It now runs NO gate. The
# gates run in the eight `Lint · *` lanes below, in parallel, and this job
# is their aggregate: the shape `TypeScript Type Check` and its
# `Type Check · *` lanes have in this file. The required context is this
# job's NAME, so the name is kept byte-identical (AGENTS.md's merge-queue
# paragraph: a required context is matched by check-run name). The lanes
# carry new names and are advisory on their own; what blocks is this
# aggregate's verdict over them.
#
# ⛔ NO GATE WAS DROPPED, WEAKENED OR GIVEN A NEW COMMAND. Every gate step of
# the single job runs in exactly one lane, on the same events, with the
# same `run:`, the same `if:`, in the same relative order and with its
# comment block. ONE exception, the ruling's: `pnpm check:pm-dispatch-gates`
# runs its fast tier on `pull_request` and `merge_group` and both tiers on
# `schedule` and `push` (argued in `scripts/pm/check-dispatch-gates.mjs`'s
# header). What each lane repeats is the six setup steps, the gate-family
# selector where the lane carries a scoped step, and, since #23191, the
# `Lane verdict` step that ends it (it replaced the unmeasured-tail
# reporter pair). Every lane keeps the single job's full-history checkout:
# which of the ~190 gates read history was not re-derived for this split,
# and a gate that loses its history degrades to "not verified" rather than
# going red.
#
# The lanes are independent. The only inter-step reads in the single job
# were `steps.gate-families.outputs.*` (nine `if:` lines, one step `id:`),
# and no step built, installed or generated a file a later step read, so no
# lane can miss an artifact another lane would have written. Each lane
# runs its gates sequentially and, since #23191, runs every one of them
# whatever the gates before it said: the EVERY GATE RUNS note above
# `jobs:` holds per lane.
#
# THE CUT, from the per-step timestamps of those 70 runs (a lane's wall
# rebuilt per run: that run's own setup steps plus the lane's steps as they
# ran there, skipped ones at zero):
#
# lane steps p50 max (minutes)
# lint-eslint 2 3.3 4.0
# lint-repo-tooling 60 4.2 4.7
# lint-docs-code 55 4.1 4.6
# lint-ci-engine 58 3.9 4.4
# lint-scoped-erasure 2 3.5 5.0
# lint-scoped-slot-corpus 3 3.5 4.9
# lint-scoped-tooling 3 1.1 4.4
# lint-pm-dispatch-gates 1 1.1 5.1 fast tier, estimated
#
# The ruled budget is 5 minutes per lane at median, and a lane over budget
# SPLITS, it does not skip: that is why the nine path-scoped families sit
# in four lanes rather than one (the eight besides the PM self-test ran 9
# minutes together). These are predictions; the real numbers are the PR's
# own CI run and every run after it. Not taken, also ruled: per-family
# path scoping for the always-on gates, and input caches. Both need a
# declaration of what a gate reads, and a wrong declaration skips a gate
# silently.
name: Lint & Repo Gates
needs:
- lint-eslint
- lint-repo-tooling
- lint-docs-code
- lint-ci-engine
- lint-scoped-erasure
- lint-scoped-slot-corpus
- lint-scoped-tooling
- lint-pm-dispatch-gates
# ── ROSTER DECLARATION (#10490), the `typecheck` aggregate's discipline ──
# The lanes this required aggregate stands in for, named rather than
# counted. `node scripts/check-aggregator-roster.mjs` (statically, in
# `lint-ci-engine`) holds this list equal to `needs:` in both directions,
# and holds this job to declaring one at all, since it carries a required
# context; the verdict step below checks the same equality against
# `toJSON(needs)` at run time. A ninth lane added to this workflow and
# forgotten here would otherwise ride green as an ADVISORY check-run behind
# the required "Lint & Repo Gates" context. This aggregate has no
# non-member inputs: every job in `needs:` is a lane whose verdict it
# carries.
env:
OS_AGGREGATOR_MEMBERS: lint-eslint lint-repo-tooling lint-docs-code lint-ci-engine lint-scoped-erasure lint-scoped-slot-corpus lint-scoped-tooling lint-pm-dispatch-gates
# `always()` is load-bearing and must not become `success()` or the
# implicit bare condition: with those, a failed lane SKIPS this job, and a
# skipped required context is an absence, which reads as green in every
# checks list and never blocks the queue. The `typecheck` job carries the
# same contract and says so at length.
if: always()
runs-on: ubuntu-latest
# ⭐ JOB WALL — measured, and the rule for every job in this file. No step in
# this workflow runs under `scripts/run-with-stall-guard.mjs` (if one ever
# does, `pnpm check:stall-guard-budget` holds its cap under its job's
# number), so a step that stops producing output — a build, an install, a
# network read that never answers — is ended by nothing else. Without the
# key GitHub's default is 360 minutes, and this context is the longest
# REQUIRED check in every merge-queue group. The queue does not wait that
# long: ruleset 12119582 (`main`) treats a required check that has not
# reported within 60 minutes (`check_response_timeout_minutes`) as failed
# and removes the entry — with no red check, and no `Merge Queue Triage`
# comment either (that workflow fires on a `failure` conclusion only, and
# it watches `CI`, not this workflow) — while the hung job holds a runner
# for up to five more hours, still `in_progress`. With the wall, a hung run
# ends `cancelled`, carrying GitHub's failure annotation "The job has
# exceeded the maximum execution time of …", before the queue's own window
# closes.
#
# Measured, before #22991 split this job into lanes: p50 15.8 / p99 32.6 /
# max 32.7 minutes (n = 81); a docs-only change ran it in about 7–11, a
# code change in 25–33. The window, for every job in this file: the 100
# most recent completed runs of this workflow, 2026-10-01T00:23Z → 03:24Z
# (96 runs read, 525 successful job rows), job `started_at` →
# `completed_at` from the jobs API.
#
# The rule, for every job in this file: about 2× the measured maximum,
# rounded to a 5-minute grain, never below 10 minutes, never above the
# queue's 60-minute window. For the single job the window bound, not 2×
# (2 × 32.7 = 65.4), and the cap was placed BELOW it, at 55: a job's clock
# starts only once a runner picks it up, after the queue's has started, so
# the five minutes are room for that pickup. 55 stays the CAP every job in
# this file sits under, the lanes below included. Above 60 the stretch
# between the queue removing an entry and this check turning `cancelled`
# only grows, so when a measured maximum nears the wall the remedy is a
# faster job (or the queue's own setting, which is the maintainer's) —
# never a higher number here. Re-measure over a fresh window before
# changing any value in this file.
#
# THIS job's own wall, since #22991: 10, the rule's floor — it runs one
# sub-second verdict and no checkout, the `typecheck` aggregate's number.
# Its clock starts only after the last lane ends, so the queue's 60 minutes
# are spent by the slowest lane's wall plus this one; every lane sizes its
# own wall in its header.
timeout-minutes: 10
permissions:
contents: read
steps:
# Fail-closed, and deliberately NOT a hand-written list of the lane names:
# the verdict iterates `toJSON(needs)`, so every job wired into `needs:`
# above is enforced, and the other half of that drift — a lane added to
# the workflow but never wired into `needs:` — is caught by comparing
# `needs:` against the declared ROSTER. A copy of the `typecheck`
# aggregate's verdict, not a shared module: that one runs with no
# checkout, and so does this. Anything that is not exactly `success`
# fails, `skipped` and `cancelled` included: a lane that never ran has
# not proved anything.
- name: Verify every lint lane succeeded
env:
OS_NEEDS: ${{ toJSON(needs) }}
run: |
set -euo pipefail
printf '%s' "$OS_NEEDS" > "$RUNNER_TEMP/needs.json"
cat > "$RUNNER_TEMP/verify-lanes.mjs" <<'NODE'
import { readFileSync } from 'node:fs';
// The roster this aggregator declares, read from the job-level
// `OS_AGGREGATOR_MEMBERS` above rather than re-typed here -- one
// declaration, two readers (this step, and the static
// scripts/check-aggregator-roster.mjs in the lint-ci-engine lane).
const declared = (process.env.OS_AGGREGATOR_MEMBERS ?? '').trim().split(/\s+/).filter(Boolean);
const declaredSet = new Set(declared);
const needs = JSON.parse(readFileSync(`${process.env.RUNNER_TEMP}/needs.json`, 'utf8'));
const ids = Object.keys(needs).sort();
let bad = 0;
// An empty declaration would make every comparison below trivially
// true. Refuse rather than report a green over an unread roster.
if (declared.length === 0) {
console.log(
'::error::this job declares no OS_AGGREGATOR_MEMBERS roster, so there is nothing to hold `needs:` to -- ' +
'refusing to report a pass (#4690).',
);
bad++;
}
for (const id of ids) {
const result = needs[id]?.result ?? '(no result reported)';
if (result === 'success') {
console.log(` ok ${id}`);
continue;
}
console.log(`::error::lint lane \`${id}\` concluded \`${result}\` -- expected \`success\`.`);
bad++;
}
for (const id of declared.filter((d) => !ids.includes(d))) {
console.log(
`::error::declared lane \`${id}\` is missing from this job's \`needs:\`, so its verdict is NOT aggregated -- ` +
`it publishes an advisory check-run and rides green behind a required context.`,
);
bad++;
}
for (const id of ids.filter((i) => !declaredSet.has(i))) {
console.log(
`::error::job \`${id}\` is in this job's \`needs:\` but not in the declared roster -- add it to ` +
`OS_AGGREGATOR_MEMBERS, or drop it from \`needs:\`.`,
);
bad++;
}
if (bad > 0) {
console.log(`check-lint-lanes: FAILED (${bad} problem(s) above).`);
process.exit(1);
}
console.log(`check-lint-lanes: OK (${ids.length} declared lane(s) succeeded: ${ids.join(', ')}).`);
NODE
node "$RUNNER_TEMP/verify-lanes.mjs"
lint-eslint:
# Lane 1 of 8 behind the required `Lint & Repo Gates` aggregate (#22991).
# The `lint` job above holds the contract, the cut and the roster: read it
# before moving a step between lanes. This lane: ESLint and the engine
# test-double contract gate, the two heaviest always-on gates, side by
# side.
name: Lint · ESLint
runs-on: ubuntu-latest
# JOB WALL = CHECKOUT BUDGET (20, on the checkout step) + BODY BUDGET, the
# scheme `typecheck-source-gates` argues for a full-history checkout. Body
# after the checkout, rebuilt per run from the 70 runs the `lint` job
# cites: max 3.3 minutes; 2 × 3.3 = 6.7, 10 on the floor. 20 + 10 = 30.
timeout-minutes: 30
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
# CHECKOUT BUDGET: `typecheck-source-gates` sizes it; the `lint` job
# above says why every lane carries it.
timeout-minutes: 20
with:
# The slot-lookup ratchet compares the baseline against its state at
# the merge base with main — the only way to see a file being ADDED
# to the grandfather list. A shallow clone has no merge base, and the
# check would degrade to "not verified" on every run.
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
- name: Setup pnpm
uses: ./.github/actions/setup-pnpm
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v6
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-v3-
- name: Install dependencies
run: pnpm install --frozen-lockfile
# No "migration registry matches its entry files" step since #22554:
# `packages/spec/src/migrations/registry.ts` is git-ignored and generated
# whole from `registry.ts.template` + `entries/` by `gen:migration-registry`,
# which `pnpm install` above already ran (spec's `prepare`, self-test
# included). There is no committed copy left to compare, and an entry the
# generator refuses fails that install step, loudly, in every job.
# Enforces the no-restricted-imports guard against @objectstack/spec root
# namespace imports (the dormant rule was never run in CI). Syntactic
# only, so no build step needed.
- name: ESLint
id: eslint
continue-on-error: true
run: pnpm lint
# Engine test-double contract gate (#4550, from #4434). A test double
# LOOSER than the implementation it replaces turns a green suite into no
# suite at all, silently, on exactly the paths a double was introduced
# for. #4434 is the worked example: DELETE /sharing/rules/:idOrName
# answered 500 for every rule and both address forms from the day it was
# written, while `deleteRule drops rule + all its grants` asserted success
# against a fake engine that accepted the one call shape ObjectQL.delete
# refuses. This holds every fake ObjectQL engine's `delete` to the real
# dispatch predicate — imported from @objectstack/objectql, not
# hand-mirrored, so it cannot drift — with the pre-existing fakes in a
# shrink-only, measured baseline. Static AST only, so it needs no build
# and belongs in this job. Runs its own --self-test first: the detector
# can be broken while every fake is fine, and a scan that quietly stops
# matching would report OK while reading nothing (#4868's family).
- name: Engine test-double contract gate
id: engine-test-double-contract-gate
continue-on-error: true
run: pnpm check:engine-double-contract
# ── THE LANE'S VERDICT (#23191): the last step, and the one that keeps a
# red gate red. The EVERY GATE RUNS note above `jobs:` argues it. ─────
#
# This is where the unmeasured-tail reporter pair stood, and its note
# said the fix for the gates a red skipped was not to run them, and ⛔ not
# `continue-on-error`, "which trades a red for silence". What holds now:
# the skipped gates DO run — every gate step above carries
# `continue-on-error` — and the trade is refused HERE: this step reads
# every gate's `outcome`, names the red ones and exits 1, so the job's
# conclusion is still red, never silent. The tail behind a red gate is
# empty by construction; behind a failed setup step this step lists the
# gates that never ran, from the `steps` context — final for every
# earlier step when this runs — rather than from the jobs API the
# reporter had to race (#18874), so no `actions: read` is needed any more.
- name: Lane verdict
if: ${{ !cancelled() }}
env:
OS_LANE_STEPS: ${{ toJSON(steps) }}
OS_LANE_JOB_STATUS: ${{ job.status }}
run: node scripts/ci/lane-verdict.mjs
lint-repo-tooling:
# Lane 2 of 8 behind `Lint & Repo Gates` (#22991; the `lint` job holds the
# cut). The always-on gates that ran from the top of the single job down
# to the two tenancy censuses: the docs and ADR anchors (still first; the
# EVERY GATE RUNS note above `jobs:` says why the order no longer
# matters), the `scripts/**` guards and
# shared-module self-tests, the PM tooling self-tests, the Claude hook
# guards, the docs authoring gates and both censuses.
name: Lint · repo & tooling gates
runs-on: ubuntu-latest
# JOB WALL = CHECKOUT BUDGET (20) + BODY BUDGET, `typecheck-source-gates`'
# scheme. Body after the checkout, rebuilt per run from the 70 runs the
# `lint` job cites: max 4.1 minutes; 2 × 4.1 = 8.1, 10 on the floor.
# 20 + 10 = 30.
timeout-minutes: 30
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
# CHECKOUT BUDGET: `typecheck-source-gates` sizes it; the `lint` job
# above says why every lane carries it.
timeout-minutes: 20
with:
# `fetch-depth: 0`: same reason as the `lint-eslint` checkout above.
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
- name: Setup pnpm
uses: ./.github/actions/setup-pnpm
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v6
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-v3-
- name: Install dependencies
run: pnpm install --frozen-lockfile
# #7484 docs anchors: `lychee.toml` sets `include_fragments = "none"`, so
# the `Check Documentation Links` job resolves a link's FILE and never its
# `#anchor` — measured with the pinned lychee 0.24.2 under the CI argv, a
# link to a heading that does not exist is reported `[200] OK`. So a
# cross-file anchor was a two-sided invariant with no owner: rename the
# heading, leave the inbound link, ship green. This resolves every internal
# `#fragment` against the destination page's real heading ids, computed
# with `github-slugger` — the same package fumadocs-core renders the site
# with, so the gate and the renderer cannot disagree about a slug.
#
# It lives in this job rather than in check-links.yml for two reasons: it
# needs a workspace install (that job is deliberately a checkout plus two
# dependency-free `node` checks), and it is REQUIRED where that lane is
# advisory. The four anchors that were already dead were fixed in the same
# PR, so this ships with no baseline and no allowlist.
#
# ── FOLDED IN, deliberately: the `file:line` anchor half ──────────────
#
# `content/docs/permissions/system-context.mdx` calls itself "the
# authority" for every behaviour keyed off `ExecutionContext.isSystem`
# and says it is "built by census over the whole repo, not by recall".
# Nothing held it to either claim, and it rotted at a measured rate: 101
# of its 111 anchors pointed at a line that no longer held what the row
# named, 19 days after the census was written, with CI green throughout.
#
# ⭐ The load-bearing direction is CENSUS -> PAGE, not page -> code. At
# the commit that motivated the card, re-resolving EVERY anchor on the
# page would have passed while it was missing 32 sites and its headline
# was 29 too low: a gate that only checks what a page already says can
# never find what the page failed to say. Measured again here on the
# shipped gate -- with an unanchored read site injected, the resolution
# checks report 0 findings and the population check names the site.
#
# It is a second command in THIS step rather than a step of its own: this
# job has no paths filter, so the census runs on code-only PRs (the ones
# that ADD a read site), and folding costs no new check context, no new
# `check:*` manifest key, and no new required-status entry for one page.
# The `--self-test` invocation is what `check:self-test-wired` requires,
# and it is the only instrument on this gate's matching rules -- a clean
# tree cannot tell a working rule from a weakened one.
#
# Invoked as `node` rather than through a `pnpm check:*` alias: see the
# GATE INVOCATION IDIOM note at the top of this file.
- name: Docs anchors resolve to real headings
id: docs-anchors-resolve-to-real-headings
continue-on-error: true
run: |
pnpm check:doc-anchors
node scripts/check-system-context-census.mjs --self-test
node scripts/check-system-context-census.mjs
# #3723 ADR anchors: code an accepted ADR governs must keep naming it.
# That incident reversed three accepted ADRs with a patch-level changeset,
# and the mechanism was simply that the edited file never mentioned them —
# so the author could not have known. Presence check only; the invariant
# travels in the failure message (scripts/adr-anchors/, one file per anchor since #6957).
#
# Since #5992 the same script also audits the premise the anchors rest on:
# an ADR number names exactly ONE decision. Three numbers were each claimed
# by two unrelated records (0010/0019/0057) — grandfathered on an explicit
# shrink-only allowlist in the script, so a FOURTH collision goes red here.
# The `--self-test` in the pnpm script provokes that red path on synthetic
# directories every run, because a collision is rare enough that nobody
# would otherwise see the gate fail before it mattered.
- name: ADR anchors + number uniqueness (governed code names its decision)
id: adr-anchors-number-uniqueness-governed-code
continue-on-error: true
run: pnpm check:adr-anchors
# @objectstack/verify stand-in erasure guard (#6399). The third and
# narrowest member of the slot-lookup and query-options erasure ratchets
# (in the scoped lanes since #22991). `checkReadCoercion` /
# `checkDateBucketParity` take their driver STRUCTURALLY
# (`CoercibleDriver` / `BucketableDriver`) so an out-of-tree driver —
# cloud's driver-turso in remote mode — runs the identical contract
# without importing a concrete driver type; that parameter type IS the
# compile-time half of the conformance, and an assertion on the argument
# deletes it for that call site while looking like a call that has it.
# #6354 / PR #6396 is the bill: ten `as never` casts, every call site of
# both helpers, all of them dead, sitting over a check that was provably
# alive — and no gate rang for either fact.
#
# The ESLint rule (`verify-stand-in/no-asserted-driver-argument`) blocks
# the assertion. This step is the half that stops the rule going dead: it
# reconciles the guarded set against what packages/verify actually exports
# in BOTH directions — so a third stand-in check cannot arrive unguarded
# and a stale entry cannot rot — and COUNTS the call sites it reached, so
# a rename or a moved import surfaces as a census that fell instead of as
# a green run over nothing. `pnpm lint` alone cannot tell "clean tree"
# from "matched nothing". Runs its own --self-test first.
- name: "@objectstack/verify stand-in erasure guard"
id: objectstack-verify-stand-in-erasure-guard
continue-on-error: true
run: pnpm check:verify-stand-in
# Rationale: `scripts/check-nul-bytes.mjs` header (#3127, #4890, #5157, #5460,
# #6984, #5579, #5681).
- name: Raw control-byte guard
id: raw-control-byte-guard
continue-on-error: true
run: pnpm check:nul-bytes
# Rationale: `scripts/check-gitlink-declared.mjs` header (#17472, #17468).
- name: Staged gitlinks are declared in .gitmodules
id: staged-gitlinks-are-declared-in-gitmodules
continue-on-error: true
run: pnpm check:gitlink-declared
# The first three of these are the shared modules the two `scripts/**`
# routing gates DELEGATE their design arguments to (#10608) — the parse
# guard below, and the entry guard in `lint-scoped-slot-corpus` since
# #22991. Both of
# those gates are
# SPELLING gates by deliberate design — `check:entry-guard` enforces that
# only `scripts/invoked-as.mjs` may read `process.argv[1]`, and
# `check:parse-guard` that every TypeScript parse goes through
# `scripts/ts-parse.mjs` — and each answers "why is spelling enough?" the
# same way: the BEHAVIOUR is pinned once, at the module, by that module's
# own `--self-test`. Neither self-test ran anywhere. So CI enforced
# *"everybody routes through the module"* and never once checked that the
# module still refuses — a regression inside `ts-parse.mjs` (a
# `process.exit` downgraded to a `throw`, a dropped `parseDiagnostics`
# read) was caught by nothing, and its symptom is the same green line this
# whole family exists to distrust (#10574 / #10573 / #4690).
#
# `js-comment-mask.mjs` is here because BOTH gates ask it to tell code
# from prose before they judge anything, and its header records that it
# exists because two private `stripComments` families drifted apart in
# opposite directions. Its two failure directions are not symmetric: a
# mask that stops masking makes both gates fabricate findings out of their
# own documentation — loud — while a mask that starts OVER-masking makes
# both go quietly green over text they never read.
#
# `import-prerequisite.mjs` is the FOURTH module here, from a different
# family and for the same reason (#14007). It is not one of the #10608
# delegates — it is the shared `PREREQUISITE NOT MET` frame that 45 gates
# IMPORT (42 under `scripts/**`, 3 under `packages/lint/scripts/**`), and
# its `--self-test` is the ONE place the inherited advisory is pinned. The
# module says so itself: *"Pinned HERE and nowhere else, because this is
# the one copy 45 importers print."* Since #13983 the same self-test also
# pins the exit-code CLASS those 45 gates return for an unmet prerequisite
# (`EXIT_PREREQUISITE_NOT_MET`, distinct from a gate's own findings exit).
# Nothing ran it: `git grep -n "import-prerequisite" -- .github/workflows
# package.json` was EMPTY, so 60 cases executed only when a human or an
# agent typed the command by hand.
#
# ⛔ NOT a hole in `check:self-test-wired`, whose green is correct by its
# own definition: its population is the scripts CI RUNS plus, since
# #21351, the scripts a root `package.json` alias self-tests, and this
# file is a MODULE no workflow can invoke and no alias names, so it sat in
# that gate's remainder by construction. The blast radius and the
# population rule point in opposite directions here: the file with the
# largest inheritance surface in the farm is the one shape the wiring gate
# cannot see. Naming it in this step is what moves it INTO that population
# (161 -> 162 scripts CI runs that
# ship a `--self-test`), and it is the smaller of the two remedies — the
# other being to widen the gate to a transitive closure, which #14007
# explicitly does not ask for. ⛔ It is also not a licence to sweep the
# rest of that remainder from here; that is a separate card.
#
# What runs here is the self-tests, not the modules' callers: for the first
# three the callers are the two routing gates named above, which already run on
# every PR; for `import-prerequisite` the callers are the 45 importing
# gates, whose ordinary runs take the branch where the dependency IS
# present and therefore observe none of what the self-test pins.
# Invoked as `node` rather than through `pnpm check:*`: see the GATE
# INVOCATION IDIOM note at the top of this file.
# `ts-parse` spawns ~20 real node children (~10 s measured, and the spawns
# are the point — they pin that a caller's try/catch cannot swallow the
# refusal); the other three are in-process fixtures, ~0.6 s combined.
#
# ⭐ Collected rather than sequenced, for the reason spelled out at the
# `Shallow-history guard self-tests` step below (#10814): under `bash -e` a
# bare sequence stops at the first failure, so a red `ts-parse` would leave
# the entry-predicate, comment-mask and prerequisite-frame self-tests UNRUN
# while the log shows only the one failure. `ts-parse` is both the slowest
# of the four and the one that spawns real children, so it is the likeliest
# to be red — precisely the masking direction. The four modules are
# independent of one another, so collecting loses nothing.
#
# ⭐ Collected is also why the fourth leg is HERE rather than in a step of
# its own: Actions skips a job's remaining steps once a step fails, so a
# standalone step further down this job would be masked by every gate above
# it — the same defect one level up, and the split `check-step-collectors`
# explicitly refuses as a remedy. Inside this block the frame's self-test
# prints its own verdict whatever the other three do, and it does so early
# in the job.
- name: scripts/ shared-module self-tests (parse · entry predicate · comment mask · prerequisite frame)
id: scripts-shared-module-self-tests-parse-entry
continue-on-error: true
run: |
# Tolerate-and-collect (#10814) — see the note above this step. Each
# self-test runs unconditionally and prints its own verdict; the step
# still FAILS when any of them does, naming every one that failed.
# ⛔ Never let the collector swallow the exit code — a green step over
# a red self-test looks identical to success. Both directions are pinned
# by `node scripts/check-step-collectors.mjs --self-test`, which extracts
# THIS block from THIS file and drives it under `bash -e` with stubs.
failed=""
run_self_test() {
echo "-- $*"
if "$@"; then
echo "PASS $*"
else
echo "FAIL $*"
failed="${failed} $*"$'\n'
fi
return 0
}
run_self_test node scripts/ts-parse.mjs --self-test
run_self_test node scripts/invoked-as.mjs --self-test
run_self_test node scripts/js-comment-mask.mjs --self-test
run_self_test node scripts/import-prerequisite.mjs --self-test
if [ -n "$failed" ]; then
echo ""
echo "scripts/ shared-module self-tests — the following FAILED:"
printf "%s" "$failed"
exit 1
fi
echo "scripts/ shared-module self-tests — all four ran and passed"
# Every `scripts/**` TypeScript parse goes through ONE module (#10133 /
# #10573), and this is the half that keeps it that way (#10574).
# NONE of the three parser entry points throws on a source it cannot
# read: `ts.createSourceFile` returns a tree built by error recovery with
# the errors parked on `parseDiagnostics`; `ts.createProgram` parks them
# behind a second call, `getSyntacticDiagnostics()`; `ts.transpileModule`
# reports nothing at all without `reportDiagnostics: true` and still
# hands back an `outputText`. A gate then walks the wreckage, finds none
# of the shapes it is looking for, and scores the file CLEAN — so the
# SYMPTOM OF A MISSING REFUSAL IS A GREEN LINE, and an unguarded gate is
# indistinguishable from a guarded one by reading CI. That is not a
# theory: one gate here forced `ScriptKind.TSX` on 2504 test files, read
# 32 of them as wreckage, and printed `OK` while six pinned engine
# doubles went uncounted.
# The #10573 sweep converted 32 call sites across 15 gates; it could not
# stop the sixteenth being typed, and within the hour a new gate landed
# with two raw calls in it — caught by this step, which is the whole
# argument for having it. Same shape as `check:entry-guard` (in
# `lint-scoped-slot-corpus` since #22991).
# Also prints the parses OUTSIDE `scripts/**` (#10575) that it does not
# govern, so its green line is read as a claim about `scripts/` and not
# about the repository.
# Scans ~121 scripts/ files plus a read-only census of the rest, no
# spawns; ~0.6s.
- name: scripts/ TypeScript parses go through one module
id: scripts-typescript-parses-go-through-one
continue-on-error: true
run: pnpm check:parse-guard
# Nobody writes a NEW private comment-stripper (#12307).
# `scripts/js-comment-mask.mjs` exists because two private `stripComments`
# families drifted apart in opposite directions — one regex-based and
# blind to string literals, opening PHANTOM comments that delete real
# code; one string-aware but regex-blind, hiding real comments. What the
# tree had no instrument for was ADOPTION: `check:comment-mask-corpus`
# below verifies the shared mask against a real parser, which is a claim
# about the MODULE and says nothing about its callers. So the module
# landed, some callers were converted by hand, and the residue was found
# by hand three separate times — three cards for one conversion.
# This gate is the half that covers the caller nobody has written yet:
# a new private stripper reds on the PR that writes it, while the 23
# measured pre-existing ones sit in a shrink-only ledger and are NOT
# required to move first (a gate that reds on day one is a gate nobody
# can land). Converting them is per-row and deliberately not this gate's
# call.
# ⚠️ NOT a widening of `check:parse-guard` above: that gate governs the
# three TypeScript PARSER ENTRY POINTS and its own header refuses a root
# broader than `scripts/**` twice. Comment-stripping is a different
# subject, and widening that population would still catch no stripper.
# Its `--self-test` runs FIRST and is where the detectors are observed
# FIRING and, just as importantly, observed STAYING SILENT on an adopter
# and on prose — the live corpus is green by construction, so a passing
# production run alone cannot tell a working detector from a dead one.
# The ledger's stale-row equality is the other half: breaking a detector
# reddens the production run (measured: 3 stale rows) instead of going
# quietly green, which is what keeps this out of the double-green family
# `check:self-test-wired` names.
# Invoked as `node` rather than through a `pnpm check:*` alias: see the
# GATE INVOCATION IDIOM note at the top of this file.
# Scans ~5k files under packages/** + examples/**, no spawns; ~2s.
- name: No new private comment-strippers (all route through js-comment-mask)
id: no-new-private-comment-strippers-all-route
continue-on-error: true
run: |
node scripts/check-comment-mask-adoption.mjs --self-test
node scripts/check-comment-mask-adoption.mjs
# The declaration-emission completeness guard (#19402). Its PRODUCTION run
# is a step of `packages/spec`'s own build, in another job, for the reason
# #11907 recorded: turbo caches only SUCCESSFUL tasks, so the verdict has
# to land inside the build or a partial emission is cached under the
# ordinary hash and replayed. What runs HERE is the `--self-test`, and it
# is the only instrument on this gate's rule: a healthy tree is green by
# construction, so the production run cannot tell a working walk from a
# dead one. The self-test supplies the partial emission a real tree does
# not contain — every declared entry present, a chunk gone, which is the
# tree both neighbouring guards acquit.
# Hermetic: real fixture files in a temp dir, no network, no node_modules
# beyond `typescript`; ~1s.
- name: Declaration references resolve (#19402) — self-test
id: declaration-references-resolve-self-test
continue-on-error: true
run: node scripts/check-dts-references.mjs --self-test
# A package's published `src/**` may only import workspace packages its own
# manifest DECLARES (#10062). The class was filed with one member and a
# mitigation — "type-only, so nothing lands in the emitted JavaScript" —
# and grew to four with no signal, one of them a VALUE import whose
# implementation the bundler then INLINED into a second package's dist.
# Both halves of the mitigation are now ledger EVIDENCE instead of prose:
# a `type-only` row reds the moment its import stops being type-only.
# Node builtins plus the shared comment mask only — no node_modules, so a
# reviewer can run it in place. Its `--self-test` runs FIRST: the
# production run over a fixed tree is green by construction, so it cannot
# tell a working matcher from a dead one, and the specifier FLOOR is the
# other half (a dead matcher finds nothing, and nothing is what success
# looks like — so it refuses with exit 2 rather than passing).
# Invoked as `node` rather than through a `pnpm check:*` alias: see the
# GATE INVOCATION IDIOM note at the top of this file.
# Scans ~2k non-test src files across 78 workspace packages, no spawns; ~1s.
- name: Published src imports only declared workspace deps
id: published-src-imports-only-declared
continue-on-error: true
run: |
node scripts/check-undeclared-dep-imports.mjs --self-test
node scripts/check-undeclared-dep-imports.mjs
# Rationale: `scripts/check-keyed-text-bounds.mjs` header (#12147, #11374, #12058,
# #12143).
- name: Keyed text-family columns declare their bound (#12147)
id: keyed-text-family-columns-declare
continue-on-error: true
run: |
node scripts/check-keyed-text-bounds.mjs --self-test
node scripts/check-keyed-text-bounds.mjs
# Rationale: `scripts/check-bash32-floor.mjs` header (#12221).
- name: shell scripts hold the bash 3.2 floor
id: shell-scripts-hold-the-bash-3-2-floor
continue-on-error: true
run: pnpm check:bash32-floor
# Stack-collection enumerations vs the schema (#6242). `stack.zod.ts`
# decides which collections a stack may declare; eight other enumerations
# of that same set are hand-maintained (the map-format list, the
# plural→singular map, the artifact category enum, ObjectQL's two
# registration loops, the artifact-ingest field map, the runtime's
# app-payload probe, the showcase coverage manifest) and nothing compared
# any of them to the schema. Each drift instance has been fixed one line at
# a time before, always after a missing key silently dropped a collection.
# A deviation is legal here only as a waiver row carrying its reason.
- name: Stack-collection enumerations answerable to stack.zod.ts
id: stack-collection-enumerations
continue-on-error: true
run: pnpm check:stack-collection-maps
# The vendored HotCRM permissions artifact has NO non-test loader (#13419).
# `security-plugin.ts` folds a POSITION name into the permission-set
# request, so a permission set named after a position is granted with no
# `sys_position_permission_set` row behind it. The #13419 census reports
# exactly two such folds — `sales_rep` and `sales_manager` — and both take
# their permission-set half from a fixture that only one TEST reads. That
# makes them a collision between an example app's position names and a
# test fixture's set names rather than grants any deployment holds, which
# is why the ruling's 物化 worklist is empty in this repo. The empty
# worklist is the thing being pinned: wire that artifact into a real
# composition and this reddens, instead of two live authorization grants
# appearing with nothing said. Its `--self-test` supplies the loader cases
# a clean tree by construction does not contain — without them the
# production zero could equally be a scanner that stopped matching.
# Invoked as `node` rather than `pnpm check:*`: see the GATE INVOCATION
# IDIOM note at the top of this file. ~1s, no spawns.
- name: 'The #13419 name-fold fixture has no non-test loader'
id: the-name-fold-fixture-has-no-non-test-loader
continue-on-error: true
run: |
node scripts/check-position-name-fold-loaders.mjs --self-test
node scripts/check-position-name-fold-loaders.mjs
# The dashboard widget `options` census vs the spec (#12926). The parser's
# `CONSUMED_WIDGET_OPTION_KEYS` decides which `options` keys get a
# `unconsumed-widget-option` warning, and five of its six members are the
# properties `DashboardWidgetOptionsSchema` declares in THIS repo's spec.
# That half was a literal pin asserted against itself, so a key added to
# the schema left the parser warning about spec-legal, renderer-consumed
# metadata — the shape that gets a diagnostic deleted by the next person
# who hits it. The sixth member (`description`) is a resolver output, kept
# as a ledger row that is checked from both ends rather than skipped.
- name: Widget `options` census derived from the spec, not pinned
id: widget-options-census-derived-from-the-spec
continue-on-error: true
run: pnpm check:widget-option-census
# The sdui-parser LOCKSTEP invariant (#12719 / #13150). Two copies of the
# constrained-JSX parser exist: objectui's runs in the RENDERER, this one
# runs in the SAVE GATE, and #12719 states that they must byte-agree on
# the accepted grammar and on the diagnostic codes — an invariant that had
# been enforced by NOTHING through three manual ports, two of which landed
# only because someone happened to file a card naming the drift.
#
# Compared against a VENDORED RECORD of objectui's side, so this step
# needs no network, no build and no objectui checkout. Read the gate's
# header for what that buys and what it costs: it sees this copy moving
# away from the recorded objectui state; it does NOT see objectui moving
# afterwards, and only a `.objectui-sha` bump — the moment a divergence
# starts SHIPPING — brings that direction back into view.
#
# Unfiltered on purpose. The record lives under packages/sdui-parser/, the
# pin is a root dotfile, and this workflow declares no paths: filtering it
# to the parser subtree would go dormant on exactly the PR that moves the
# pin. Cost is one TypeScript parse of seven small files, ~0.4 s.
- name: sdui-parser stays in lockstep with objectui's copy
id: sdui-parser-stays-in-lockstep-with-objectui