Repository navigation
Expand file tree
/
Copy pathgenerate-projections
More file actions
executable file
·1079 lines (1024 loc) · 64.3 KB
/
Copy pathgenerate-projections
File metadata and controls
executable file
·1079 lines (1024 loc) · 64.3 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
#!/usr/bin/env bash
# Emit each fact into every document that states it, from the one place that owns it (ADR-0034 §4.5).
#
# TWO SOURCES, ONE MECHANISM. The coordination protocol comes from the engine's `Protocol.fs`; the
# version leads come from `registry/dependencies.yml` (#913). A region names its source in its own
# marker and in every drift message, because "where did this number come from" is the only question a
# reader of a generated region ever has, and two sources make it answerable wrongly.
#
# THE INVERSION. A coordination rule was stated in up to six places — the ADR, the canonical doc, the
# tool, and four `SKILL.md` bodies across two skill roots — then content-addressed into `repos.lock` and
# byte-copied into six receivers. **54 vendored copies.** The propagation edge cost a second issue and a
# second PR every time (#309 → #502, #481 → #531), and the collision attractor was removed BY HAND twice
# (#532, #551) before #570 gated it.
#
# `fsgg-coord` was ALREADY the model — it was simply not the SOURCE. In every drift that can be dated,
# the tool was right and the prose was wrong. So the rules now live once, in the typed core that already
# enforces them (`FS.GG.Coord.Core/Protocol.fs`), and the prose is a BUILD ARTIFACT: `repos.lock`'s
# discipline, applied to the protocol itself. A collision in a generated region is a rebase, not a
# decision — and a rule cannot land in one tier and not the others, because there are no tiers.
#
# The proof it was needed arrived while the flip was being written: `TouchSetGrammar` was typed into F#
# by hand, byte-identical to bash's `TOUCHSET_GRAMMAR` purely by luck, with NOTHING holding them in step.
# A seventh copy, added by the change that was fixing the copies. That cross-check has since moved into
# the type system — see THE GRAMMAR HAS ONE HOME NOW, below.
#
# scripts/generate-projections # rewrite every generated region in place
# scripts/generate-projections --check # verify they are current; exit 1 on any drift (the GATE)
# scripts/generate-projections --list # print `kind<TAB>path<TAB>BEGIN-marker` per region; the gate's subject list
# scripts/generate-projections --contract-version
# # print the derived version of the agent contract
#
set -euo pipefail
HERE="$(cd "$(dirname "$0")/.." && pwd)"
# Projection subjects are repo-owned paths. A symlink in any component can make
# --check read outside that tree while still reporting a current local target.
reject_symlink_chain() {
local relative="$1" cursor="$HERE" component remaining="$1"
while [ -n "$remaining" ]; do
component="${remaining%%/*}"
if [ "$remaining" = "$component" ]; then remaining=""; else remaining="${remaining#*/}"; fi
cursor="$cursor/$component"
if [ -L "$cursor" ]; then
echo "::error::$relative crosses a symlink at $cursor" >&2
exit 2
fi
done
}
CHECK=""
LIST=""
CONTRACT_VERSION=""
case "${1:-}" in
--check) CHECK=1 ;;
--list) LIST=1 ;;
--contract-version) CONTRACT_VERSION=1 ;;
"") ;;
*) echo "::error::unknown argument: $1 (expected --check, --list, --contract-version, or nothing)" >&2; exit 2 ;;
esac
# The operator's agent contract is the complete byte surface under both live skill roots. Its version
# is derived from one canonical stream: sorted root-relative path, byte length, then bytes, for every
# regular file. Paths and lengths make the encoding injective without assuming skill files contain no
# NUL bytes. This function is the ONE spelling of that stream; tests and callers consume the command's
# result rather than reimplementing it. A root that cannot be read or contains no files is a non-answer,
# never the digest of an accidentally empty contract.
agent_contract_version() {
python3 - "$HERE" <<'PY'
import hashlib
import pathlib
import sys
repo = pathlib.Path(sys.argv[1])
roots = (pathlib.Path(".agents/skills"), pathlib.Path(".claude/skills"))
entries: list[tuple[str, pathlib.Path]] = []
for root in roots:
absolute = repo / root
if not absolute.is_dir():
raise SystemExit(f"::error::agent contract root is missing or unreadable: {root}")
files = [path for path in absolute.rglob("*") if path.is_file()]
if not files:
raise SystemExit(f"::error::agent contract root contains no files: {root}")
entries.extend((path.relative_to(repo).as_posix(), path) for path in files)
digest = hashlib.sha256()
for relative, path in sorted(entries):
name = relative.encode("utf-8")
body = path.read_bytes()
digest.update(len(name).to_bytes(8, "big"))
digest.update(name)
digest.update(len(body).to_bytes(8, "big"))
digest.update(body)
print(digest.hexdigest())
PY
}
if [ -n "$CONTRACT_VERSION" ]; then
agent_contract_version
exit 0
fi
# ---- the targets ---------------------------------------------------------------------------------
# Each target names the region kind it carries. The canonical doc and the skill that OWNS the protocol
# take `rules`; the driver skill takes the contract its loop reads. BOTH skill roots, always: `.agents/`
# is a byte-for-byte twin of `.claude/`, so a generator that wrote only one would leave the duplication
# it was built to remove, one level down.
#
# THIS LIST IS DECLARED BEFORE THE ENGINE IS NEEDED, so `--list` can answer without one. The non-vacuity
# gate reads it to learn WHICH documents to check, rather than carrying a second hand-copied list of them
# (the first copy of that list drifted into `projections.yml` and had to be edited in step, by hand).
# The gate still hardcodes HOW MANY it expects — deriving the count from here too would let deleting a
# target satisfy the gate vacuously, which is the #266 shape this guard exists to refuse.
TARGETS=(
"rules:docs/coordination/parallel-work.md"
"rules:.claude/skills/intra-repo-parallel-work/references/protocol-facts.md"
"rules:.agents/skills/intra-repo-parallel-work/references/protocol-facts.md"
"take-exit-codes:.claude/skills/pnext-item/references/command-contracts.md"
"take-exit-codes:.agents/skills/pnext-item/references/command-contracts.md"
"landable-exit-codes:.claude/skills/pnext-item/references/command-contracts.md"
"landable-exit-codes:.agents/skills/pnext-item/references/command-contracts.md"
"release-columns:.claude/skills/pnext-item/references/merge-and-release.md"
"release-columns:.agents/skills/pnext-item/references/merge-and-release.md"
"filing-rules:.claude/skills/cross-repo-coordination/references/mailbox-and-board.md"
"filing-rules:.agents/skills/cross-repo-coordination/references/mailbox-and-board.md"
"reconcile-rules:.claude/skills/check-board/references/mechanical-reconciliation.md"
"reconcile-rules:.agents/skills/check-board/references/mechanical-reconciliation.md"
"driver-rules:.claude/skills/pnext-item/references/command-contracts.md"
"driver-rules:.agents/skills/pnext-item/references/command-contracts.md"
"wave-policy:.claude/skills/drive-board/references/host-loop.md"
"wave-policy:.agents/skills/drive-board/references/host-loop.md"
"wave-policy:.claude/skills/work-board/references/host-loop.md"
"wave-policy:.agents/skills/work-board/references/host-loop.md"
"review-policy:.claude/skills/pnext-item/references/independent-review.md"
"review-policy:.agents/skills/pnext-item/references/independent-review.md"
"lifecycle-policy:.claude/skills/pnext-item/references/independent-review.md"
"lifecycle-policy:.agents/skills/pnext-item/references/independent-review.md"
"ledger-policy:.claude/skills/pnext-item/references/independent-review.md"
"ledger-policy:.agents/skills/pnext-item/references/independent-review.md"
"release-inventory:.claude/skills/publishing-and-deployment/SKILL.md"
"release-inventory:.agents/skills/publishing-and-deployment/SKILL.md"
"blocker-states:.claude/skills/check-board/references/mechanical-reconciliation.md"
"blocker-states:.agents/skills/check-board/references/mechanical-reconciliation.md"
"board-statuses:.claude/skills/cross-repo-coordination/references/mailbox-and-board.md"
"board-statuses:.agents/skills/cross-repo-coordination/references/mailbox-and-board.md"
"snapshot-shape:.claude/skills/check-board/references/mechanical-reconciliation.md"
"snapshot-shape:.agents/skills/check-board/references/mechanical-reconciliation.md"
"versions:docs/architecture.md"
"contract-versions:docs/registry/compatibility.md"
"skill-registry-counts:docs/registry/compatibility.md"
"component-count:docs/components.md"
"component-inventory:docs/components.md"
"component-inventory:docs/consumer/versioning-and-updates.md"
)
# The marker a kind uses. One function, so the BEGIN a target is searched for, the BEGIN written into
# it, and the BEGIN the gate asserts cannot disagree — `--list` emits this rather than making the gate
# re-derive it, because a mapping stated in two places is the drift this whole file exists to refuse.
#
# `rules` keeps the original unnamed spelling (three files already carry it); renaming it would rewrite
# three documents to no end. `versions` is NOT `fsgg-protocol:*` on purpose: every other region is
# emitted from the engine's `Protocol.fs`, and this one is emitted from `registry/dependencies.yml`. A
# marker names its SOURCE, and calling the registry "protocol" would be a lie in the one place a reader
# looks to find out where a number came from.
marker() { # kind, BEGIN|END
case "$1" in
rules) echo "<!-- $2 GENERATED: fsgg-protocol -->" ;;
versions) echo "<!-- $2 GENERATED: fsgg-versions -->" ;;
contract-versions) echo "<!-- $2 GENERATED: fsgg-contract-versions -->" ;;
skill-registry-counts) echo "<!-- $2 GENERATED: fsgg-skill-registry-counts -->" ;;
component-count) echo "<!-- $2 GENERATED: fsgg-component-count -->" ;;
component-inventory) echo "<!-- $2 GENERATED: fsgg-component-inventory -->" ;;
release-inventory) echo "<!-- $2 GENERATED: fsgg-release-inventory -->" ;;
*) echo "<!-- $2 GENERATED: fsgg-protocol:$1 -->" ;;
esac
}
# The FILE a kind is emitted from. There are two sources now, so "this region is stale" has to name the
# right one: a drift report that sends the reader to Protocol.fs for a number that lives in the registry
# is worse than no report — it is a wrong answer with a gate's authority behind it, and the reader's next
# move is to hand-edit the generated region, which is the one thing every marker here tells them not to do.
source_of() { # kind
case "$1" in
versions) echo "registry/dependencies.yml" ;;
contract-versions) echo "registry/dependencies.yml" ;;
skill-registry-counts) echo "registry/skills.yml" ;;
component-count) echo "registry/repos.yml" ;;
component-inventory) echo "registry/repos.yml + registry/dependencies.yml" ;;
release-inventory) echo "registry/dependencies.yml" ;;
*) echo "src/FS.GG.Coord.Core/Protocol.fs" ;;
esac
}
if [ -n "$LIST" ]; then
for spec in "${TARGETS[@]}"; do
kind="${spec%%:*}"
printf '%s\t%s\t%s\n' "$kind" "${spec#*:}" "$(marker "$kind" BEGIN)"
done
exit 0
fi
# THE ENGINE IS THE SOURCE FOR THE PROTOCOL REGIONS, AND IT IS BUILT FROM SOURCE HERE. `.github` never
# takes the engine from the feed — that asymmetry is deliberate (ADR-0034 §4.3): fixing coord would
# otherwise require publishing, and publishing would require coord to schedule the fix.
#
# It is required unconditionally even though `versions` does not read it, because this script rewrites
# EVERY region on every run: a mode that regenerated only the registry-sourced ones would be a way to
# leave the protocol regions stale without noticing, and `--check` has to answer for all of them anyway.
ENGINE="${FSGG_COORD_ENGINE_BIN:-$HERE/src/FS.GG.Coord.Cli/bin/Release/net10.0/fsgg-coord-engine}"
if [ ! -x "$ENGINE" ]; then
echo "::error::no engine at $ENGINE — build it: dotnet build src/FS.GG.Coord.Cli -c Release" >&2
exit 2
fi
FACTS="$(mktemp)"
REG="$(mktemp)"
SKILLS="$(mktemp)"
ROSTER="$(mktemp)"
trap 'rm -f "$FACTS" "$REG" "$SKILLS" "$ROSTER"' EXIT
"$ENGINE" facts --json >"$FACTS"
# THE SECOND SOURCE. `versions` is emitted from the REGISTRY, not the engine — the two regions project
# different truths and neither is the other's source. Python is a YAML→JSON transcoder here and nothing
# more: every projection in this file renders through `jq`, so the rendering has one shape regardless of
# which source it reads.
#
# WHY THE REGISTRY IS ALLOWED TO BE A SOURCE AT ALL. A projection is only as honest as the thing it
# projects, and generating a wrong number just centralises the lie (#916). The registry earns it by
# being gated against reality on both scalars it carries: `check-feed-coherence.py` asserts every
# `package-version` against the newest version live on the org feed, and `check-source-coherence.py`
# asserts `fsgg-contracts`' `version` against the FS.GG.Contracts source SemVer on FS.GG.SDD@main. The
# numbers below are therefore held to the feed by a gate, not by whoever last edited the page.
#
# Checked up front, in the same spirit as the missing-renderer check below: without this the run dies on
# a `ModuleNotFoundError` traceback, which is fail-closed but names PYTHON's problem rather than the
# author's. The engine gets a sentence telling you how to fix it; so should this.
if ! python3 -c 'import yaml' 2>/dev/null; then
echo "::error::PyYAML is not installed, and the 'versions' region is emitted from registry/dependencies.yml — install it: python -m pip install pyyaml" >&2
exit 2
fi
reject_symlink_chain registry/dependencies.yml
python3 -c '
import json, sys, yaml
json.dump(yaml.safe_load(open(sys.argv[1])), sys.stdout)
' "$HERE/registry/dependencies.yml" >"$REG"
# A SECOND registry source, on the same terms. `skill-registry-counts` is emitted from
# `registry/skills.yml` — the org skill catalog — and the same "why is a registry allowed to be a
# source" argument holds one level over: the counts are a pure projection of the catalog's ROW SET,
# which is itself held to reality by `skill-registry-coherence` (`fsgg-skill-registry-check` reconciles
# every row's `sha256`/`materializes-when` against the producer manifests). So counting the rows here
# centralises no lie — it removes a hand-maintained count (#486 reconciled it by hand once; #1206 is
# this) whose only failure mode was a human miscounting a set a gate already owns.
reject_symlink_chain registry/skills.yml
python3 - "$HERE/registry/skills.yml" >"$SKILLS" <<'PY'
import json
import re
import sys
import yaml
def refuse(message):
print(f"::error::registry/skills.yml {message}", file=sys.stderr)
raise SystemExit(2)
class UniqueLoader(yaml.SafeLoader):
pass
def mapping(loader, node):
result = {}
for key_node, value_node in node.value:
key = loader.construct_object(key_node)
if not isinstance(key, str) or key in result:
refuse(f"has a non-string or duplicate YAML key: {key!r}")
result[key] = loader.construct_object(value_node)
return result
UniqueLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, mapping)
try:
with open(sys.argv[1]) as stream:
catalog = yaml.load(stream, Loader=UniqueLoader)
except (OSError, yaml.YAMLError, ValueError) as error:
refuse(f"cannot be read uniquely: {error}")
if not isinstance(catalog, dict) or catalog.get("schemaVersion") != 3:
refuse("has no supported schemaVersion 3 mapping")
rows = catalog.get("skills")
if not isinstance(rows, list) or not rows:
refuse("has no nonempty skills list")
seen = set()
token = re.compile(r"[a-z0-9_.-]+\Z")
scopes = {"process", "product", "driver", "operator"}
for index, row in enumerate(rows):
if not isinstance(row, dict):
refuse(f"skills[{index}] is not a mapping")
skill_id, scope, owner = row.get("id"), row.get("scope"), row.get("owner")
if not isinstance(skill_id, str) or not token.fullmatch(skill_id):
refuse(f"skills[{index}] has an unsafe or missing id")
if skill_id in seen:
refuse(f"skills[{index}] repeats id {skill_id!r}")
seen.add(skill_id)
if not isinstance(scope, str) or scope not in scopes:
refuse(f"skills[{index}] has an unsupported scope")
if not isinstance(owner, str) or not token.fullmatch(owner):
refuse(f"skills[{index}] has an unsafe or missing owner")
json.dump(catalog, sys.stdout)
PY
# A THIRD registry source, on the same terms. The consumer-facing count and component-inventory regions
# (#1313) are emitted from `registry/repos.yml` — the org repo ROSTER (ADR-0019) — joined with the
# `repos:`/`contracts:` blocks of `registry/dependencies.yml` for each component's description and live
# version. The roster is the ONE authoritative list of the framework repos every org fabric iterates,
# gated by `scripts/repos.sh` and `check-roster-closure.py`; the version leads are held to the live feed
# by `check-feed-coherence.py` (see the transcode comment above). So counting the roster's framework rows
# and projecting each component's `role` + version here centralises no lie — it removes the hand-typed
# component COUNT and the hand-maintained component table that rotted the whole consumer-doc surface
# (roadmap §3a: "five repositories" / "four components" wrong in a dozen places).
# `default=str` because registry/repos.yml carries an unquoted `updated:` YAML date, which PyYAML loads
# as a `datetime.date` that json.dump cannot serialize (dependencies.yml/skills.yml quote theirs). The
# rendering never reads `updated`, so stringifying it is harmless — it just keeps the transcode total.
reject_symlink_chain registry/repos.yml
python3 -c '
import json, sys, yaml
json.dump(yaml.safe_load(open(sys.argv[1])), sys.stdout, default=str)
' "$HERE/registry/repos.yml" >"$ROSTER"
# ---- the projections -----------------------------------------------------------------------------
# ONE rendering PER REGION KIND, and a document says which kind it carries. There is still exactly one
# place each fact is rendered — what a target chooses is WHICH facts it states, not how they read.
#
# The kinds exist because the driver skills are not the canonical doc (#889). `/pnext-item` restates
# `take`'s exit contract and nothing else about the scheduler; emitting the full rules block into it to
# retire that one table would bury a seven-line contract under seventy lines of material it already
# links to `intra-repo-parallel-work` for. A region carries what its document is FOR.
# `rules` — the scheduler's rules and its verdict vocabulary. The canonical doc and the skill that owns
# the protocol. This is the original unnamed region; its marker keeps the original spelling, because
# renaming it would rewrite three files to no end.
render_rules() {
echo "<!--"
echo " DO NOT EDIT THIS REGION. It is emitted from src/FS.GG.Coord.Core/Protocol.fs by"
echo " scripts/generate-projections, and \`projections\` in CI fails on any diff."
echo ""
echo " This region exists because a rule stated in six documents is a rule that will disagree with"
echo " itself — #485 (startability computed in five places, agreeing in none) and the #502/#531/#551"
echo " family. Edit Protocol.fs and regenerate; a collision here is a rebase, not a decision (#309)."
echo "-->"
echo ""
echo "### The rules the scheduler actually enforces"
echo ""
echo "*Generated from the typed core. The engine that decides your item is the engine that wrote this.*"
echo ""
jq -r '.rules[] | "#### \(.title)\n\n\(.statement)\n\n> **Why:** \(.because)\n"' "$FACTS"
echo "### What the scheduler can tell you, and nothing else"
echo ""
echo "One total function returns one of these. There is no other answer, and there is no silent no —"
echo "an unreachable answer is not a negative one."
echo ""
jq -r '.verdicts[] | "- **`\(.kind)`** — \(.meaning)"' "$FACTS"
echo ""
}
# The BODY of an exit-code table, for a given `facts` key. `take`'s contract and `landable`'s are the
# same SHAPE — a code, what it means, what to do — so they share one renderer and differ only in the
# prose above them. A second copy of this would re-open the drift this file exists to close, and the
# `|` escaping below is exactly the subtlety that rots in a copy: it is invisible until some fact grows
# a pipe, and then it corrupts a table that still renders.
render_exit_code_table() { # facts-key
echo "| exit | meaning | what to do |"
echo "|---|---|---|"
# `|` is the CELL SEPARATOR, so a pipe inside a fact would silently split one cell into two and shift
# every column after it — a corrupted table that still renders, which is the worst way for this to
# fail. Escape it here rather than forbidding it in Protocol.fs: the prose should not have to know
# what it is being rendered into.
jq -r --arg key "$1" '
def cell: gsub("\\|"; "\\|");
.[$key][]
| "| **\(.code)**\(if .name == "" then "" else " (`" + (.name | cell) + "`)" end) | \(.meaning | cell) | \(.action | cell) |"' "$FACTS"
echo ""
}
# `take-exit-codes` — `take`'s exit contract (#585), for the driver skill whose loop reads it.
render_take_exit_codes() {
echo "<!--"
echo " DO NOT EDIT THIS REGION. It is emitted from src/FS.GG.Coord.Core/Protocol.fs by"
echo " scripts/generate-projections, and \`projections\` in CI fails on any diff."
echo ""
echo " The hand-written copy of this table was WRONG for as long as it existed (#889): it documented"
echo " EX_PARTIAL — a write that half-landed — as \`take\` failing to READ the board, and its \"≠0, ≠2\""
echo " row swallowed every other row in the table. Edit Protocol.fs and regenerate."
echo "-->"
echo ""
render_exit_code_table takeExitCodes
}
# `landable-exit-codes` — `landable`'s POLL-LOOP contract (#900), for the same driver skill. §5 restated
# it by hand and restated BASH's numbers, so `3` was documented as "pending" when the engine returns it
# for "red" — a loop built from the table hangs forever on a PR that will never go green.
render_landable_exit_codes() {
echo "<!--"
echo " DO NOT EDIT THIS REGION. It is emitted from src/FS.GG.Coord.Core/Protocol.fs by"
echo " scripts/generate-projections, and \`projections\` in CI fails on any diff."
echo ""
echo " The hand-written copy of this table documented BASH's codes (#900) — green 0, pending 3, red 1"
echo " — where the engine returns 0/7/3/4 and keeps 3 == red across every verdict command. It was"
echo " wrong in BOTH directions on the two codes a poll loop reads. Edit Protocol.fs and regenerate."
echo "-->"
echo ""
render_exit_code_table landableExitCodes
}
# `release-columns` — `release`/`reap`'s column precedence (#1099), for the driver skill's "Abandoning
# an item" section. The THIRD table in the class #889/#900 proved: `release`'s column semantics is the
# org's most-repaired behaviour (seven issues), and it was the one #1059 restatement that is a
# PRECEDENCE rather than a `Rule`, so it could not join `driverRules`. `writes` renders as yes/no; the
# `stdout` templates are wrapped as code because they are literal output a worker matches against.
render_release_columns() {
echo "<!--"
echo " DO NOT EDIT THIS REGION. It is emitted from src/FS.GG.Coord.Core/Protocol.fs by"
echo " scripts/generate-projections, and \`projections\` in CI fails on any diff."
echo ""
echo " This precedence was hand-authored here, and it is the single most-repaired behaviour in the"
echo " org: seven issues (#331/#354/#531/#867/#911/#914/#921) corrected a prose copy of it while the"
echo " engine's \`unclaimColumn\` was, eventually, right. #889/#900 proved the cure — the two exit-code"
echo " tables that were GENERATED have never drifted since. This is the third. Edit Protocol.fs and"
echo " regenerate."
echo "-->"
echo ""
echo "| release sees | the column becomes | writes? | stdout — the tell |"
echo "|---|---|---|---|"
# `|` is the CELL SEPARATOR, so a pipe inside a fact would silently split one cell and shift every
# column after it. Escape it here rather than forbidding it in Protocol.fs.
jq -r '
def cell: gsub("\\|"; "\\|");
.releaseColumns[]
| "| \(.condition | cell) | \(.endState | cell) | \(if .writes then "yes" else "no" end) | `\(.stdout | cell)` |"' "$FACTS"
echo ""
}
# `filing-rules` — the rules a worker FILING an item must satisfy (#889), for `cross-repo-coordination`.
#
# THE SUBSET IS CHOSEN IN `Protocol.fs`, NOT HERE. `facts --json` states `filingRules` outright, so this
# renderer selects nothing: a `jq` filter picking ids out of `.rules` would be a second, hand-maintained
# copy of the membership — living in the shell, where no test can reach it — and the rule that belongs in
# a projection is a decision about the PROTOCOL, not about how a document renders.
#
# THE `Because` IS OMITTED, and that is the difference from `rules`. `Rule.Because` is the incident that
# bought the rule; the canonical doc carries it because a reader there is learning WHY the protocol is
# shaped this way. A filer is mid-task and needs the rule they must satisfy — this skill links to
# `intra-repo-parallel-work` for the reasoning, which is the same division that makes the kind exist.
render_filing_rules() {
echo "<!--"
echo " DO NOT EDIT THIS REGION. It is emitted from src/FS.GG.Coord.Core/Protocol.fs by"
echo " scripts/generate-projections, and \`projections\` in CI fails on any diff."
echo ""
echo " These rules were restated by hand here, and #916 measured why that is not survivable: the"
echo " hand-written copies AGREED with each other for as long as they existed and were wrong the"
echo " whole time. Edit Protocol.fs and regenerate."
echo "-->"
echo ""
echo "*Generated from the typed core. The engine that refuses your \`Paths:\` line is the engine that"
echo "wrote this. The full rule set, with the incident behind each one, is in"
echo "[intra-repo-parallel-work](../../intra-repo-parallel-work/SKILL.md).*"
echo ""
jq -r '.filingRules[] | "**\(.title)**\n\n\(.statement)\n"' "$FACTS"
}
# `reconcile-rules` — the rules a RECONCILER must satisfy (#889), for `check-board`.
#
# THE SUBSET IS CHOSEN IN `Protocol.fs`, NOT HERE, for the reason `render_filing_rules` states: a `jq`
# filter picking ids would be a second copy of the membership, in the shell, where no test can reach it.
# With two subsets that stops being a preference — the shell would hold two memberships and nothing
# would hold either in step. `RuleSubsetTests` pins that `facts --json` states each subset under its own
# name, so this renderer selecting nothing is enforced rather than merely intended.
#
# WHAT IS NOT HERE, and deliberately: `check-board`'s finding codes. `CLOSED-ISSUE-NOT-DONE`,
# `BLOCKER-CLEARED`, `UNDECLARED-PATHS` and the rest are that skill's OWN procedure — decisions it makes
# about what to report and what to refuse to fix — and the engine holds no opinion about them. A region
# carries the protocol its document restates, not the document's whole subject (#916).
render_reconcile_rules() {
echo "<!--"
echo " DO NOT EDIT THIS REGION. It is emitted from src/FS.GG.Coord.Core/Protocol.fs by"
echo " scripts/generate-projections, and \`projections\` in CI fails on any diff."
echo ""
echo " These rules were restated by hand here, and a RECONCILER restating them is the sharpest form"
echo " of the problem: this pass exists to correct the projection, so a rule it gets wrong is a rule"
echo " it enforces WRONG across the whole board. The hand-written copies agreed with each other for"
echo " as long as they existed (#916). Edit Protocol.fs and regenerate."
echo "-->"
echo ""
echo "*Generated from the typed core. The engine that resolves your blockers is the engine that wrote"
echo "this. The full rule set, with the incident behind each one, is in"
echo "[intra-repo-parallel-work](../../intra-repo-parallel-work/SKILL.md).*"
echo ""
jq -r '.reconcileRules[] | "**\(.title)**\n\n\(.statement)\n"' "$FACTS"
}
# `driver-rules` — the rules a worker DRIVING an item must satisfy (#889/#1059), for `pnext-item`.
#
# THE SUBSET IS CHOSEN IN `Protocol.fs`, NOT HERE, for the reason `render_filing_rules` states.
#
# THIS IS THE FILE THE ORG HAS REPAIRED MOST, and the repairs are what argue for the region. Five times
# these four sentences were wrong while the engine was right: `release --status` called a silent no-op
# after #914 made it work (#921); every rate limit called GraphQL after #897 told three apart (#907);
# `pendingBoardWrites` called a field that never existed, 42 minutes after it shipped (#1030). Two of the
# five were closed by GENERATING the table, and neither has drifted since. This region is the third.
#
# IT REPLACES §0's PREMISE, NOT §0's RITUAL, and that line is the whole design of the kind. Why an id
# must be distinct is `claim-lock`'s — the total order separates workers only while no two share an id
# (#419). The MINT ITSELF is a command a worker pastes, and it stays authored: `worker-id-attractor.yml`
# requires exactly one runnable mint idiom naming `scripts/fsgg-coord`, which is a fact about a shell,
# not about the protocol. Emitting a second copy of that command here would be a second idiom to keep in
# step — the very thing that gate exists to refuse (#569/#570).
render_driver_rules() {
echo "<!--"
echo " DO NOT EDIT THIS REGION. It is emitted from src/FS.GG.Coord.Core/Protocol.fs by"
echo " scripts/generate-projections, and \`projections\` in CI fails on any diff."
echo ""
echo " This recipe restated these rules by hand for its whole life, and #1059 counted the cost: five"
echo " repairs to four sentences, every one of them the PROSE moving to where the engine already was."
echo " The two that were closed by generating a table have never drifted again. Two hand-written"
echo " copies also disagreed OUT LOUD — §3 said an expired lease cannot be renewed and §6 said"
echo " \`heartbeat\` renews it — and nothing could see it, because neither was the engine's answer."
echo " Edit Protocol.fs and regenerate."
echo "-->"
echo ""
echo "*Generated from the typed core. The engine that takes your claim, holds your lease and refuses"
echo "your \`Paths:\` line is the engine that wrote this. The full rule set, with the incident behind"
echo "each one, is in [intra-repo-parallel-work](../../intra-repo-parallel-work/SKILL.md).*"
echo ""
jq -r '.driverRules[] | "**\(.title)**\n\n\(.statement)\n"' "$FACTS"
}
render_wave_policy() {
echo "*Generated operational fact: the parser and driver consume this policy; do not restate its numbers.*"
echo ""
jq -r '"<!-- fsgg:wave-model:v1 waves=\(.wavePolicy.waves) implementer-slots-per-wave=\(.wavePolicy.implementerSlotsPerWave) review-slots=\(.wavePolicy.reviewSlots) consolidation-threshold=\(.wavePolicy.consolidationThreshold) -->\n\n**Two concurrent waves, a fixed \((.wavePolicy.waves * .wavePolicy.implementerSlotsPerWave) + .wavePolicy.reviewSlots)-slot cap.** Each wave holds \(.wavePolicy.implementerSlotsPerWave) implementer slots; \(.wavePolicy.reviewSlots) slots are reserved fleet-wide for independent critics. **Consolidation:** \(.wavePolicy.consolidationThreshold) or fewer active items consolidates before a fresh wave is started."' "$FACTS"
echo ""
}
render_review_policy() {
echo "*Generated structured review contract. The digest validator and state machine consume these exact values.*"
echo ""
jq -r '"| fact | value |\n|---|---|\n| schema | `\(.reviewPolicy.schema)` |\n| kinds | `\(.reviewPolicy.kinds | join(", "))` |\n| ordinary repair ceiling | \(.reviewPolicy.maxAutomatedRepairRounds) |\n| repair-phase ceiling | \(.reviewPolicy.repairPhaseMaxRounds) |"' "$FACTS"
echo ""
}
render_lifecycle_policy() {
echo "*Generated lifecycle boundary. These are machine-owned prerequisites; judgement about the work remains authored.*"
echo ""
jq -r '"Required housekeeping: " + (.lifecyclePolicy.requiredHousekeeping | map("`" + . + "`") | join(", ")) + ".\n\nHost acceptance fields: " + (.lifecyclePolicy.hostAcceptanceFields | map("`" + . + "`") | join(", ")) + ".\n\nTerminal transition evidence: " + (.lifecyclePolicy.terminalActions | map("`" + . + "`") | join(" → ")) + "."' "$FACTS"
echo ""
}
render_ledger_policy() {
echo "*Generated ledger schema. The receipt id binds these fields; prose does not substitute for the ledger.*"
echo ""
jq -r '"Schema: `\(.ledgerPolicy.schema)`.\n\nObservation fields: " + (.ledgerPolicy.observationFields | map("`" + . + "`") | join(", ")) + ".\n\nReceipt fields: " + (.ledgerPolicy.receiptFields | map("`" + . + "`") | join(", ")) + "."' "$FACTS"
echo ""
}
render_release_inventory() {
echo '*Generated from every registry package-bearing contract. Package, producer and coherent-set counts are derived here; release judgement remains below.*'
echo ""
jq -r '([.contracts[] | select(has("package-version"))] | "Registry release inventory: \(length) package-bearing contracts across \(map(.owner) | unique | length) producers; \(map(.owner + ":" + .["package-version"]) | unique | length) coherent release sets.")' "$REG"
echo ""
echo "| owner | contract | source version | published version | coherent set | surface |"
echo "|---|---|---|---|---|---|"
jq -r '
.contracts[]
| select(has("package-version"))
| "| `\(.owner)` | `\(.id)` | `\(.version)` | `\(.["package-version"])` | `\(.owner + ":" + .["package-version"])` | \(.surface | gsub("\\|"; "\\\\|") | gsub("\\n"; " ")) |"' "$REG"
echo ""
}
# `blocker-states` — the blocker WIRE vocabulary (#889), for the skill that reads it with `jq`.
#
# This is the sharpest region in the file, and the reason is mechanical rather than rhetorical. Every
# other region misleads a HUMAN when it drifts; this one is selected on by a `.state == "merged"` filter
# in `check-board` §3, so a drifted string matches NOTHING and the pass reports a clean board over items
# rotting behind shipped work (#476). Its worst output, by its own account — and silent.
#
# `holds` is emitted as the ✓/✗ column rather than left to the prose, because that bit is what a reader
# ACTS on and burying it in a sentence is how `unknown` — a non-answer that BLOCKS — reads as "fine".
#
# WHAT IS NOT HERE: §3's remedies (resolve `unknown` over REST, re-write an `unparseable` field by hand)
# and its finding codes. Those are `check-board`'s OWN procedure — what it does about a state — and the
# engine holds no opinion about them. A region carries the protocol its document restates, not the
# document's whole subject (#916, and `render_reconcile_rules` above).
render_blocker_states() {
echo "<!--"
echo " DO NOT EDIT THIS REGION. It is emitted from src/FS.GG.Coord.Core/Protocol.fs by"
echo " scripts/generate-projections, and \`projections\` in CI fails on any diff."
echo ""
echo " The hand-written copy NAMED its own source — \"the five cases of the engine's \`BlockerState\`\""
echo " — and was still a copy. Naming a source is not reading it. Generatable only since #1012 gave"
echo " the vocabulary an owner in Core; before that it was two private INVERSE copies outside it, and"
echo " typing the cases into Protocol.fs would have been a THIRD (#865)."
echo ""
echo " TWO sources, because this region states two different facts. The \`.state\` strings and the case"
echo " list come from Types.fs (\`BlockerState\`, \`blockerStateWireName\`) — that is where a NEW state or"
echo " a renamed one belongs. The \`holds?\` bit and the prose are Protocol.fs. Regenerate either way."
echo "-->"
echo ""
echo "*Generated from the typed core: \`Types.blockerStateWireName\` writes these strings, so the engine"
echo "that emits \`.state\` is the engine that wrote this table. Lower case, deliberately — an issue's own"
echo "\`state\` is UPPER case on the wire and a blocker's is not, and the two conventions are not to be"
echo "\"unified\" (§3).*"
echo ""
echo "| \`.state\` | holds? | what it means |"
echo "|---|---|---|"
# `|` is ESCAPED, and this is not defensive noise: the prose it replaced was literally
# `open | closed | merged | unknown | unparseable`, so a pipe is the single most likely character
# for an author of `blockerMeaning` to reach for. An unescaped one ends the cell, silently — and
# `--check` compares generated to generated, so it would never notice.
jq -r '.blockerStates[]
| "| `\(.wire)` | \(if .holds then "**YES**" else "no" end) | \(.meaning | split("|") | join("\\|")) |"' "$FACTS"
}
# `snapshot-shape` — the snapshot document's TOP-LEVEL shape (#889, #1058), for the skill whose every
# `jq` filter starts at one of these keys.
#
# THE READER OF THIS REGION IS A `jq` FILTER, which is what makes it worth generating rather than
# proof-reading. `check-board` §1 hands its whole pass a selector derived from this shape; a selector that
# matches nothing does not error — it returns empty, and empty renders as "nothing wrong with the board"
# (#476). So this region's failure mode is a FALSE CLEAN, the worst output that skill has.
#
# The literal this replaced was wrong twice and nobody noticed, because nothing compared it to the writer:
# it spelled `leaseMinutes` before `limit` and `inFlight` before `items`, where `Scan.snapshot` emits
# `limit` and `items` first. It also hardcoded `leaseMinutes: 90` against an engine default of 120 — which
# is the tell that a reader cannot distinguish an EXAMPLE from a CONTRACT when both are hand-typed.
#
# `reconciled` is emitted as its own column rather than left to the prose, for `render_blocker_states`'s
# reason exactly: it is the bit a reader ACTS on. Only `items` carries board facts; the rest are the scan's
# own parameters echoed back, and a pass that selects on those reconciles against its own request.
render_snapshot_shape() {
echo "<!--"
echo " DO NOT EDIT THIS REGION. It is emitted from src/FS.GG.Coord.Core/Protocol.fs by"
echo " scripts/generate-projections, and \`projections\` in CI fails on any diff."
echo ""
echo " The hand-written literal this replaced was ILLUSTRATIVE and read as NORMATIVE — the ambiguity"
echo " was the defect, not any one of its errors. It had the key order wrong in two places and a"
echo " leaseMinutes that matched no default."
echo ""
echo " TWO sources, as blocker-states has. The schema string and the key list are Protocol.fs"
echo " (snapshotSchema, snapshotKeys); the document they describe is written by Scan.snapshot and"
echo " read by Snapshot.parse, and ScanRoundTripTests PINS Protocol against both — Core states a"
echo " shape it does not render, which is #1058's ownership call and the test is its price."
echo "-->"
echo ""
echo "*Generated from the typed core. The keys are in the order \`Scan.snapshot\` WRITES them, and"
echo "\`ScanRoundTripTests\` drives the real writer to assert it — so this table cannot disagree with the"
echo "document \`scan --json\` actually emits. \`reconciled\` is the column that matters: it says which"
echo "keys a reconciler may select on.*"
echo ""
echo "\`\`\`json"
# The ENVELOPE, derived from the same key list as the table — so the order shown is the order stated,
# and neither can drift from the writer without `ScanRoundTripTests` going red. The old literal spelled
# this blob by hand and had the order wrong twice; a shape re-typed beside the table it illustrates is
# exactly the second copy this region exists to delete.
jq -r '"{ " + ([ .snapshotDocument.keys[]
| if .key == "schema" then "\"schema\": \"" + $schema + "\""
elif .key == "items" or .key == "inFlight" then "\"" + .key + "\": [ … ]"
else "\"" + .key + "\": …" end ] | join(", ")) + " }"' \
--arg schema "$(jq -r '.snapshotDocument.schema' "$FACTS")" "$FACTS"
echo "\`\`\`"
echo ""
echo "| key | reconciled? | what it carries |"
echo "|---|---|---|"
# `|` is ESCAPED for `render_blocker_states`'s reason: an unescaped pipe in a meaning ends the cell,
# silently, and `--check` compares generated to generated so it would never notice.
jq -r '.snapshotDocument.keys[]
| "| `\(.key)` | \(if .reconciled then "**YES**" else "no" end) | \(.meaning | split("|") | join("\\|")) |"' "$FACTS"
}
# `board-statuses` — the six `Status` options `cross-repo-coordination` spelled by hand, in the field
# table a filer reads before setting a column (#889, #1057).
#
# The bite is ASYMMETRIC, which is what makes it worth generating rather than proof-reading. A filer who
# copies a drifted option is safe: `set-field` refuses an unknown one, loudly, before it writes. A
# RECONCILER is not — it selects `.status` in `jq`, a drifted string matches nothing, no rows come back,
# and no rows reads as a CLEAN BOARD (#476).
#
# TWO sources, as `blocker-states` has: the option strings and the case list come from Types.fs
# (`BoardStatus`, `statusWireName`); `startable?` is Schedulability.fs (`columnStartability`), and the
# prose is Protocol.fs. Regenerate for any of them.
render_board_statuses() {
echo "<!--"
echo " DO NOT EDIT THIS REGION. It is emitted from src/FS.GG.Coord.Core/Protocol.fs by"
echo " scripts/generate-projections, and \`projections\` in CI fails on any diff."
echo ""
echo " These six strings were a table row here, hand-kept, in BOTH skill roots. A drifted option is"
echo " not a misprint: a reconciler selects it in jq, matches nothing, and reports a CLEAN BOARD over"
echo " a board it never read (#476)."
echo ""
echo " SIX, NOT SEVEN. \`BoardStatus\` has a seventh case — \`NoStatus\`, wire form \`\"\"\` — and it is"
echo " the ABSENCE of a column rather than an option a filer can select. Protocol.fs drops it in a"
echo " total match, so a new case cannot join the union without being classified."
echo "-->"
echo ""
echo "*Generated from the typed core: \`Types.statusWireName\` writes these option names, so the engine"
echo "that reads a column back is the engine that wrote this table. \`startable?\` is"
echo "\`Schedulability.columnStartability\` — the predicate \`take\` schedules on, not a copy that agrees"
echo "with it today.*"
echo ""
echo "| option | startable? | what it means |"
echo "|---|---|---|"
# `|` is ESCAPED for `render_blocker_states`'s reason: an unescaped pipe in the prose ends the cell
# silently, and `--check` compares generated to generated, so it would never notice.
#
# THREE ANSWERS, NOT A CHECKBOX. `with-backlog-opt-in` is rendered as its own answer and NAMES THE FLAG
# that starts it. Rounding it to "no" would publish the board's most common park as plain unstartable
# and hide `--include-backlog`; to "yes", it would promise a queue a bare `take` never reads.
#
# THE FOURTH CASE ERRORS RATHER THAN RENDERING "no", and that is not defensive noise. A `ColumnStartability`
# case added in F# reaches this filter as a string it has never seen, and an `else "no"` would publish
# the new case as NOT STARTABLE — a confident wrong answer, in a generated region nobody proof-reads,
# about the one column a worker is deciding whether to look in. The engine's total match cannot help
# here: it is on the other side of a JSON boundary. So the untyped end fails LOUD (#266).
jq -r '.boardStatuses[]
| "| `\(.wire)` | \(
if .startable == "always" then "**YES**"
elif .startable == "with-backlog-opt-in" then "only with `--include-backlog`"
elif .startable == "never" then "no"
else error("unknown startable value \"\(.startable)\" for board status \"\(.wire)\" — scripts/generate-projections renders three ColumnStartability cases and the engine emitted a fourth; teach render_board_statuses what it means rather than letting it render as \"no\"") end
) | \(.meaning | split("|") | join("\\|")) |"' "$FACTS"
}
# `versions` — the registry's version leads (#913), for the architecture map. THE ONLY REGION EMITTED
# FROM THE REGISTRY RATHER THAN THE ENGINE.
#
# The map hand-wrote version claims the registry already owned, and each value appeared in three or four
# places on ONE page — so a bump was a four-site hand-edit and any missed site was a silent
# self-contradiction. #910 had to hand-repair four at once: Game 0.4.0 (registry: 0.5.0), Audio 0.2.0
# (0.3.0), `ContractVersion 1.4.0` (2.0.1 — stale by a MAJOR), `minimum-fsgg-sdd 0.4.0` (0.6.0). The
# tier-1/tier-2 flips landed the bumps and never touched the map, because nothing made them.
#
# THE ROWS ARE DERIVED, NOT LISTED. Every contract carrying a `package-version` gets one. A hand-listed
# subset would be the #916 trap — an enumeration only beats a catch-all if it is COMPLETE — and a new
# package-bearing contract would be silently absent from the projection that claims to be the map's
# source. Same for the orchestrator axis: any contract growing a `minimum-fsgg-sdd` states one.
#
# WHAT THIS REGION DELIBERATELY DOES NOT SWALLOW. Only CURRENT-STATE claims belong to the registry.
# Dated history — "advanced 0.4.0→0.6.0 on 2026-07-04", "re-pinned at 0.1.0 in FS.GG.Rendering#238" —
# is true forever, cannot drift, and is NOT the registry's to state; generating over it would flatten
# prose the page needs and assert facts this document does not own (#913's own boundary note).
render_versions() {
echo "<!--"
echo " DO NOT EDIT THIS REGION. It is emitted from registry/dependencies.yml by"
echo " scripts/generate-projections, and \`projections\` in CI fails on any diff."
echo ""
echo " This region exists because this page was the one version-bearing projection with no generator"
echo " and no gate (#913). Every number below is the registry's, and the registry is held to the live"
echo " feed by check-feed-coherence.py. Edit registry/dependencies.yml and regenerate; if a number"
echo " here is wrong, the registry is wrong, and fixing it HERE would only hide that."
echo "-->"
echo ""
echo "*Generated from \`registry/dependencies.yml\`. \`version\` is the contract SURFACE's SemVer and"
echo "\`package-version\` is what is LIVE on the org feed: different facts, and publish-before-flip"
echo "(FR-007) means \`package-version\` never leads \`version\` — so read them together, because"
echo "\`version\` alone never tells you what a consumer can restore. One exception the schema itself"
echo "carves: for \`fs-gg-ui-template\`, \`version\` is the FRAMEWORK pin (\`FsGgUiVersion\` — the generated"
echo "product's \`FS.GG.UI.*\` pin), which is a different axis from the template package's"
echo "\`package-version\`, not a lagging copy of it.*"
echo ""
echo "| Contract | Owner | \`version\` | \`package-version\` |"
echo "|---|---|---|---|"
# `// error(...)`, not a bare lookup: an owner missing from `repos:` would otherwise interpolate as
# the literal string "null" and be SPLICED INTO THE MAP as a fact. A generator's failure mode must be
# a red gate, never a confident wrong render — the whole point of this region is that the page stops
# being able to state a version nobody vouched for.
jq -r '
.repos as $repos
| .contracts[]
| select(has("package-version"))
| . as $c
| ($repos[.owner].name // error("contract `\($c.id)` names owner `\($c.owner)`, which is not a key in repos:"))
| "| `\($c.id)` | \(.) | `\($c.version)` | `\($c["package-version"])` |"
' "$REG"
echo ""
jq -r '
.contracts[]
| select(has("minimum-fsgg-sdd"))
| "**The orchestrator axis.** `\(.id)` pins `minimum-fsgg-sdd` at **`\(.["minimum-fsgg-sdd"].version)`** — the oldest published `fsgg-sdd` that seeds the artifacts a workspace on this pin is expected to contain (ADR-0008; see *The coherent set has three axes* below)."
' "$REG"
echo ""
}
# `contract-versions` — the machine-owned half of docs/registry/compatibility.md, split from its
# hand-authored prose (#1081 option 2, DECIDED 2026-07-17; the `repos.lock`/#527 move, ADR-0044).
#
# WHY THIS REGION EXISTS. The REQUIRED `projection` gate (scripts/check-projection.py) asserts that
# every contract's `version` and `package-version` literal is projected into compatibility.md. It used
# to read the literal from the hand-authored `Version` CELL of the "Versioned contracts" table — a cell
# that also carries kilobytes of authored release prose. So a registry flip demanded editing a prose
# cell, and #748 forbids `feed-autofix` to write prose: the gate and the bot were each correct and
# jointly unsatisfiable, and the bot's flips were structurally red (#1081). Splitting the literal out
# into this generated region gives it one owner — the generator — so a flip REGENERATES green, and the
# prose cell stays human-owned. `check-projection.py` now reads the literal from HERE.
#
# EVERY CONTRACT, NOT JUST THE PACKAGE-BEARING ONES. `render_versions` (the architecture map) lists only
# contracts carrying a `package-version`, because the map states what a consumer can restore. This region
# is the subject of a gate that iterates ALL contracts and checks each `version`, so it must carry a row
# for every contract or the gate reds "has no row" on the ones it omits (#916: an enumeration only beats a
# catch-all if it is COMPLETE). A contract with no `package-version` states `—` for that column.
render_contract_versions() {
echo "<!--"
echo " DO NOT EDIT THIS REGION. It is emitted from registry/dependencies.yml by"
echo " scripts/generate-projections, and \`projections\` in CI fails on any diff."
echo ""
echo " This is the MACHINE-OWNED half of this page (#1081 option 2 / ADR-0044 / #527). The REQUIRED"
echo " \`projection\` gate (scripts/check-projection.py) reads each contract's version literal from"
echo " THIS table, not from the authored prose \`Version\` cell in the Versioned contracts table"
echo " above, so a registry flip regenerates this region green rather than demanding a prose edit the"
echo " feed-autofix bot is forbidden to make (#748). Every number below is the registry's; if one is"
echo " wrong, the registry is wrong, and fixing it HERE would only hide that."
echo "-->"
echo ""
echo "*Generated from \`registry/dependencies.yml\`. \`version\` is the contract SURFACE's SemVer;"
echo "\`package-version\` is what is LIVE on the org feed (\`—\` where the contract ships no package)."
echo "This table is the machine-owned literal; the judgement about what a release MEANS stays in the"
echo "authored \`Version\` cells of the Versioned contracts table (#748).*"
echo ""
# Plain column headers (not backticked) so the REQUIRED `projection` gate can locate the `version`
# and `package-version` columns by name without stripping markup — this table is a gate subject first
# and a human read second.
echo "| Contract | Owner | version | package-version |"
echo "|---|---|---|---|"
# `// error(...)`, not a bare lookup: an owner missing from `repos:` must red the gate, never
# interpolate the literal string "null" as a fact (same discipline as render_versions).
jq -r '
.repos as $repos
| .contracts[]
| . as $c
| ($repos[.owner].name // error("contract `\($c.id)` names owner `\($c.owner)`, which is not a key in repos:"))
| "| `\($c.id)` | \(.) | `\($c.version)` | \(if ($c | has("package-version")) then "`\($c["package-version"])`" else "—" end) |"
' "$REG"
echo ""
}
# The skill-registry ROW COUNTS. Emitted from registry/skills.yml (#1206). The counts were restated in
# hand-authored prose in several places — the dependency graph, the `skill-registry` contract row, the
# `skill-registry-published` coherence row — phrased differently each time, and drifted (a stale `25 =
# 17+8` sat beside a current `27 = 18+9`). #486 reconciled them by hand once; this makes them a real
# projection so a reconcile is `generate-projections`, not a recount. A pure count of a set a gate
# already owns centralises no lie (see the transcode comment above).
render_skill_registry_counts() {
echo "<!--"
echo " DO NOT EDIT THIS REGION. It is emitted from registry/skills.yml by"
echo " scripts/generate-projections, and \`projections\` in CI fails on any diff."
echo ""
echo " The skill-registry row counts were woven into hand-authored prose in several places, phrased"
echo " differently each time, and drifted — #486 reconciled them by hand once, #1206 turned them into"
echo " this projection. Each number is a pure count of registry/skills.yml's rows, whose SET is held"
echo " to the producer manifests by skill-registry-coherence; if a number here is wrong, the catalog"
echo " is wrong, and fixing it HERE would only hide that. Authored prose that still states a count"
echo " should CITE this block, not restate it."
echo "-->"
echo ""
echo "*Generated from \`registry/skills.yml\` — the authoritative breakdown of the skill catalog by"
echo "\`scope\` and \`owner\`. This is the machine-owned count; the prose elsewhere on this page cites it.*"
echo ""
jq -r '
.skills as $s
| ($s | length) as $total
| ([$s[] | select(.scope=="process")] | length) as $proc
| ([$s[] | select(.scope=="product")]) as $prod
| ([$s[] | select(.scope=="driver")] | length) as $drv
| ([$s[] | select(.scope=="operator")] | length) as $op
| ($prod | group_by(.owner) | map("\(length) `\(.[0].owner)`") | join(" + ")) as $prodBy
| "**\($total) rows** = **\($proc) process** + **\($prod | length) product**"
+ (if $drv > 0 then " + **\($drv) driver**" else "" end)
+ (if $op > 0 then " + **\($op) operator**" else "" end)
+ " (\($prodBy))."
' "$SKILLS"
echo ""
echo "| scope | owner | rows |"
echo "|---|---|---|"
jq -r '
.skills
| group_by([.scope, .owner])
| map({scope: .[0].scope, owner: .[0].owner, n: length})
| sort_by(.scope, .owner)[]
| "| \(.scope) | `\(.owner)` | \(.n) |"
' "$SKILLS"
jq -r '.skills | "| **total** | | **\(length)** |"' "$SKILLS"
echo ""
}
# `component-count` — the framework/repository COUNT (#1313), emitted from registry/repos.yml. THE FIRST
# consumer-facing region, and the second emitted from the roster rather than the engine.
#
# WHY THIS REGION EXISTS. The component/repository count was hand-typed into a dozen consumer-facing
# places — the org profile, four component READMEs, five consumer-guide files — and rotted the instant
# Game/Audio/Net were added: "five repositories" and "four components" were wrong across the whole
# surface (roadmap §3a). It is now a pure count of the roster's rows, so "add the eighth component" is a
# roster edit that every doc stating the count follows, with no hand edit (ADR-0058 derive-don't-restate).
render_component_count() {
echo "<!--"
echo " DO NOT EDIT THIS REGION. It is emitted from registry/repos.yml by"
echo " scripts/generate-projections, and \`projections\` in CI fails on any diff."
echo ""
echo " The component/repository COUNT was hand-typed into a dozen consumer-facing places and rotted"
echo " the instant Game/Audio/Net were added — \"five repositories\" / \"four components\" were wrong in"
echo " the org profile, four component READMEs and five consumer-guide files (roadmap §3a, #1313). It"
echo " is now a pure count of registry/repos.yml's roster rows, whose SET is held closed by"
echo " check-roster-closure.py; add or retire a repo THERE and every doc that states the count follows."
echo "-->"
echo ""
jq -r '
def word:
{"1":"one","2":"two","3":"three","4":"four","5":"five","6":"six","7":"seven","8":"eight",
"9":"nine","10":"ten","11":"eleven","12":"twelve"}[tostring]
// error("registry/repos.yml states a repo count outside the number-word map (1-12) in render_component_count — extend it");
def cap: (.[0:1] | ascii_upcase) + .[1:];
# THE SENTENCE IS ARITHMETIC, so its two numbers must come from the same set (.github#2245).
# $total was (.repos | length) while the sentence reads "those $fw plus this .github coordination
# repo", which held only while EVERY roster row was an FS-GG-owned fabric participant — the one
# shape the roster could express. It can now hold a non-participant row (EHotwagner/S.I.R., per
# .github#2206), and counting it here would render "seven framework components ... across nine
# repositories in the org (those seven plus this .github coordination repo)" — a self-
# contradicting count, in the org profile, about a repository the org does not even own. The
# subject of this sentence is the org fabric, so the count is the fabric rows.
([.repos[] | select(.role != "non-participant")] | length) as $total
| ([.repos[] | select(.role == "framework")] | length) as $fw
| "**\($fw | word | cap) framework components** ship independently — each public on [nuget.org](https://www.nuget.org) and restoring with no credential (ADR-0039) — across **\($total | word)** repositories in the org (those \($fw | word) plus this `.github` coordination repo)."
' "$ROSTER"
echo ""
}
# `component-inventory` — the component-inventory table (#1313), emitted from registry/repos.yml joined
# with registry/dependencies.yml. Carried by the profile page AND the consumer "what ships" guide, so
# both state the SAME inventory from one source (the projection philosophy: a fact in every doc that
# states it, from the one place that owns it).
#
# THE ROW SET IS THE ROSTER, NOT A HAND LIST. Every framework row in registry/repos.yml gets a row; the
# description is that component's `role` in registry/dependencies.yml, and the version is its
# package-bearing contracts' live `package-version`. So "add the eighth component" (a roster row) grows
# every table, and "bump a package" (a contracts flip) updates every version — the two edits roadmap §3a
# names as the rot, closed at their source. Package IDs are STABLE identity, not a rot vector
# (readme-standard.md), so the exact `dotnet add package` / `dotnet tool install` lines stay AUTHORED
# beside this table rather than generated into it — the registry carries no consumer package-id list.
render_component_inventory() {
echo "<!--"
echo " DO NOT EDIT THIS REGION. It is emitted from registry/repos.yml + registry/dependencies.yml by"
echo " scripts/generate-projections, and \`projections\` in CI fails on any diff."
echo ""
echo " The component inventory was a hand-maintained table in the profile page and the consumer"
echo " 'what ships' guide, and it rotted: Game/Audio/Net shipped without ever being added (roadmap"
echo " §3a, #1313). The ROW SET is now the framework rows of registry/repos.yml's roster; each row's"
echo " description is that component's \`role\` in registry/dependencies.yml, and the version is its"
echo " package-bearing contracts' live \`package-version\` (held to the feed by check-feed-coherence.py)."
echo " Add a framework repo to the roster and a row appears; bump a package and the version follows —"
echo " with no hand edit to any consumer doc. If a description reads too technically, fix the \`role\`"
echo " in registry/dependencies.yml (the one home), not a copy here."
echo "-->"
echo ""
echo "*Generated from \`registry/repos.yml\` (the org repo roster, ADR-0019) joined with"
echo "\`registry/dependencies.yml\` (each component's \`role\` and its contracts' live \`package-version\`)."
echo "\`Current version\` is \`—\` for a component whose packages are not (yet) tracked as a"
echo "package-bearing contract owned by that component. The exact acquire command and package IDs are"
echo "authored beside this table — package IDs are stable identity, versions are not (readme-standard).*"
echo ""
echo "| Component | What it does | Current version |"
echo "|---|---|---|"
# `// error(...)`, not a bare lookup: a framework repo the roster names but dependencies.yml does not
# describe must RED the gate, never render "null" as a fact — the same discipline render_versions uses.
# `|` is ESCAPED (cell) so a pipe in a `role` cannot silently split the cell and shift every column.
jq -r --slurpfile dep "$REG" '
def cell: gsub("\\|"; "\\|");
$dep[0] as $d
| .repos
| map(select(.role == "framework"))[]
| .id as $id
| .full as $full
| ($d.repos[$id] // error("roster framework repo `\($id)` has no entry in registry/dependencies.yml repos: — the inventory cannot state what it does")) as $r
| ([ $d.contracts[] | select(.owner == $id and has("package-version")) | .["package-version"] ] | unique | sort) as $vers
| "| [**\($r.name)**](https://github.com/\($full)) | \($r.role | rtrimstr("\n") | gsub("\n"; " ") | cell) | \(if ($vers | length) > 0 then ($vers | map("`" + . + "`") | join(" / ")) else "—" end) |"
' "$ROSTER"
echo ""
}
drift=0
for spec in "${TARGETS[@]}"; do
kind="${spec%%:*}"
rel="${spec#*:}"
f="$HERE/$rel"
reject_symlink_chain "$rel"
[ -f "$f" ] || { echo "::error::missing projection target: $rel" >&2; exit 2; }
# A kind with no renderer would otherwise die as `render_foo: command not found` — fail-closed, but it
# names bash's problem rather than the author's.
if ! declare -F "render_${kind//-/_}" >/dev/null; then
echo "::error::$rel declares region kind '$kind', which has no render_${kind//-/_}() in this script" >&2
exit 2
fi
b="$(marker "$kind" BEGIN)"
e="$(marker "$kind" END)"