@bongos/core 1.21.2 → 1.21.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +33 -23
- package/docs/copy-inventory.md +5 -5
- package/docs/copy-registry.json +5 -5
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +3 -3
- package/migrations/core_271_bongos_shipper_badge_name.sql +14 -0
- package/migrations/core_272_rename_cn_criterion_slugs.sql +40 -0
- package/modules/hall-ui/public/roster.js +26 -27
- package/modules/lifecycle/criterion-ref.js +11 -0
- package/modules/lifecycle/db-tasks.js +7 -0
- package/modules/lifecycle/routes/done-when.js +6 -0
- package/modules/status-ui/public/status.js +28 -27
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/tests/craft_achievements.mjs +58 -19
- package/tests/criterion_goal_attach.mjs +12 -4
- package/tests/criterion_ref_parse.mjs +41 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.21.
|
|
6
|
-
"core_contract": "1.21.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.21.4",
|
|
6
|
+
"core_contract": "1.21.4",
|
|
7
|
+
"source_commit": "227ae34b0487683d4edf44ffe2bc124e642bdad7",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-
|
|
9
|
+
"built_at": "2026-10-02T02:09:36.609Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 576,
|
|
13
13
|
"agent_docs_stubbed": 27,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2849,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3453,
|
|
20
|
+
"tree_sha256": "745f91270b3cdca0328608ed1dad6684e2aebe6a52a1fe7e294d9016458f470b",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2307,12 +2307,12 @@
|
|
|
2307
2307
|
{
|
|
2308
2308
|
"path": "docs/copy-inventory.md",
|
|
2309
2309
|
"mode": "0000644",
|
|
2310
|
-
"sha256": "
|
|
2310
|
+
"sha256": "10381700ed5d3a63cf56babc5f4843b82ea19240569e51dc2f4904e792d0f2d2"
|
|
2311
2311
|
},
|
|
2312
2312
|
{
|
|
2313
2313
|
"path": "docs/copy-registry.json",
|
|
2314
2314
|
"mode": "0000644",
|
|
2315
|
-
"sha256": "
|
|
2315
|
+
"sha256": "73db034f2d5cffd41f54bde2869e5a5188e355ad821c5402c2a864d4f3da1e52"
|
|
2316
2316
|
},
|
|
2317
2317
|
{
|
|
2318
2318
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -2817,7 +2817,7 @@
|
|
|
2817
2817
|
{
|
|
2818
2818
|
"path": "docs/module-api-changelog.md",
|
|
2819
2819
|
"mode": "0000644",
|
|
2820
|
-
"sha256": "
|
|
2820
|
+
"sha256": "53996e5c9e4a988cff115ec80dc2d61b9f16fa151c4c96fb6ccd2b5e1b6b6a29"
|
|
2821
2821
|
},
|
|
2822
2822
|
{
|
|
2823
2823
|
"path": "docs/modules-contract.md",
|
|
@@ -2907,7 +2907,7 @@
|
|
|
2907
2907
|
{
|
|
2908
2908
|
"path": "docs/page-readings.json",
|
|
2909
2909
|
"mode": "0000644",
|
|
2910
|
-
"sha256": "
|
|
2910
|
+
"sha256": "c7a34b3c1d5a248b7c386e8135ae44330114969eb67cbea54513581af4e6f023"
|
|
2911
2911
|
},
|
|
2912
2912
|
{
|
|
2913
2913
|
"path": "docs/project-context.template.md",
|
|
@@ -4024,6 +4024,16 @@
|
|
|
4024
4024
|
"mode": "0000644",
|
|
4025
4025
|
"sha256": "106be8e1efc153460d1dffc809598498f5f91063beb7799e36f025b9aceb7723"
|
|
4026
4026
|
},
|
|
4027
|
+
{
|
|
4028
|
+
"path": "migrations/core_271_bongos_shipper_badge_name.sql",
|
|
4029
|
+
"mode": "0000644",
|
|
4030
|
+
"sha256": "7b05f548a3498aa472267ca10b26236cc81fa52df0334be9ec89c9d620866e2f"
|
|
4031
|
+
},
|
|
4032
|
+
{
|
|
4033
|
+
"path": "migrations/core_272_rename_cn_criterion_slugs.sql",
|
|
4034
|
+
"mode": "0000644",
|
|
4035
|
+
"sha256": "a9a435bf6e5d6bc94f6029a9375519c80041cbac8d791f98989dffc09b5b1dfe"
|
|
4036
|
+
},
|
|
4027
4037
|
{
|
|
4028
4038
|
"path": "modules/agents/CLAUDE.md",
|
|
4029
4039
|
"mode": "0000644",
|
|
@@ -5927,7 +5937,7 @@
|
|
|
5927
5937
|
{
|
|
5928
5938
|
"path": "modules/hall-ui/public/roster.js",
|
|
5929
5939
|
"mode": "0000644",
|
|
5930
|
-
"sha256": "
|
|
5940
|
+
"sha256": "6039efe77849f1284f13226b8e9b0cd5f307372df15f52e481261d507a5d0e83"
|
|
5931
5941
|
},
|
|
5932
5942
|
{
|
|
5933
5943
|
"path": "modules/hall-ui/public/roster.states.json",
|
|
@@ -6567,7 +6577,7 @@
|
|
|
6567
6577
|
{
|
|
6568
6578
|
"path": "modules/lifecycle/criterion-ref.js",
|
|
6569
6579
|
"mode": "0000644",
|
|
6570
|
-
"sha256": "
|
|
6580
|
+
"sha256": "90f77854fb0d0c7c70d9bd20f018100e555c72920840e84afe24789531f1db2f"
|
|
6571
6581
|
},
|
|
6572
6582
|
{
|
|
6573
6583
|
"path": "modules/lifecycle/criterion-suggest.js",
|
|
@@ -6647,7 +6657,7 @@
|
|
|
6647
6657
|
{
|
|
6648
6658
|
"path": "modules/lifecycle/db-tasks.js",
|
|
6649
6659
|
"mode": "0000644",
|
|
6650
|
-
"sha256": "
|
|
6660
|
+
"sha256": "2efcf47f6240b7a21e1d908440a04e3f0af3e86ca22aed5b74ba03ed354de437"
|
|
6651
6661
|
},
|
|
6652
6662
|
{
|
|
6653
6663
|
"path": "modules/lifecycle/db-versions.js",
|
|
@@ -6992,7 +7002,7 @@
|
|
|
6992
7002
|
{
|
|
6993
7003
|
"path": "modules/lifecycle/routes/done-when.js",
|
|
6994
7004
|
"mode": "0000644",
|
|
6995
|
-
"sha256": "
|
|
7005
|
+
"sha256": "a6adbf715fb5f2994a0f4603859e5047b321311b66e04d9fcb96ce9ce854c5b3"
|
|
6996
7006
|
},
|
|
6997
7007
|
{
|
|
6998
7008
|
"path": "modules/lifecycle/routes/gate-approvals.js",
|
|
@@ -9277,7 +9287,7 @@
|
|
|
9277
9287
|
{
|
|
9278
9288
|
"path": "modules/status-ui/public/status.js",
|
|
9279
9289
|
"mode": "0000644",
|
|
9280
|
-
"sha256": "
|
|
9290
|
+
"sha256": "1e470dc9924196deaa2d92c314086a8e9a77fb446e015c74a3927a3de0f70712"
|
|
9281
9291
|
},
|
|
9282
9292
|
{
|
|
9283
9293
|
"path": "modules/status-ui/public/style.css",
|
|
@@ -9597,12 +9607,12 @@
|
|
|
9597
9607
|
{
|
|
9598
9608
|
"path": "package-lock.json",
|
|
9599
9609
|
"mode": "0000644",
|
|
9600
|
-
"sha256": "
|
|
9610
|
+
"sha256": "b2c580b1a1dac4d154f0210d9a505aff2793d869c4a9daf2e74662684924e71b"
|
|
9601
9611
|
},
|
|
9602
9612
|
{
|
|
9603
9613
|
"path": "package.json",
|
|
9604
9614
|
"mode": "0000644",
|
|
9605
|
-
"sha256": "
|
|
9615
|
+
"sha256": "ea4e035d42d25f49b75519085179ae3162c28a9ea263d47db99532354c4e4c4b"
|
|
9606
9616
|
},
|
|
9607
9617
|
{
|
|
9608
9618
|
"path": "public-docs/index.html",
|
|
@@ -9622,7 +9632,7 @@
|
|
|
9622
9632
|
{
|
|
9623
9633
|
"path": "release-notes.json",
|
|
9624
9634
|
"mode": "0000644",
|
|
9625
|
-
"sha256": "
|
|
9635
|
+
"sha256": "39ceec2a9fbb1e216f935e73b23f3a380ba319100a13f99bf71bc3783d90e987"
|
|
9626
9636
|
},
|
|
9627
9637
|
{
|
|
9628
9638
|
"path": "scripts/bongos-mcp.js",
|
|
@@ -11837,7 +11847,7 @@
|
|
|
11837
11847
|
{
|
|
11838
11848
|
"path": "src/module-api.js",
|
|
11839
11849
|
"mode": "0000644",
|
|
11840
|
-
"sha256": "
|
|
11850
|
+
"sha256": "59ddf172c5c74312f7140b65decd20aa6e6e54778ba47b7d3b78913fdea45d4a"
|
|
11841
11851
|
},
|
|
11842
11852
|
{
|
|
11843
11853
|
"path": "src/module-loader/catalog.js",
|
|
@@ -12857,7 +12867,7 @@
|
|
|
12857
12867
|
{
|
|
12858
12868
|
"path": "tests/craft_achievements.mjs",
|
|
12859
12869
|
"mode": "0000644",
|
|
12860
|
-
"sha256": "
|
|
12870
|
+
"sha256": "0fede138a51800506d1edba81eb46fc04eb49509ebe942d9834247d12d606d81"
|
|
12861
12871
|
},
|
|
12862
12872
|
{
|
|
12863
12873
|
"path": "tests/craft_mode.mjs",
|
|
@@ -12892,7 +12902,7 @@
|
|
|
12892
12902
|
{
|
|
12893
12903
|
"path": "tests/criterion_goal_attach.mjs",
|
|
12894
12904
|
"mode": "0000644",
|
|
12895
|
-
"sha256": "
|
|
12905
|
+
"sha256": "49a2d85d679669efd63c0b2600bd2f207106f37f00cb5beebcd0e6a4775b6452"
|
|
12896
12906
|
},
|
|
12897
12907
|
{
|
|
12898
12908
|
"path": "tests/criterion_progress.mjs",
|
|
@@ -12907,7 +12917,7 @@
|
|
|
12907
12917
|
{
|
|
12908
12918
|
"path": "tests/criterion_ref_parse.mjs",
|
|
12909
12919
|
"mode": "0000644",
|
|
12910
|
-
"sha256": "
|
|
12920
|
+
"sha256": "8373511dace3ecb37d88961d22a9f8fb2f01fb34d12a0075d287b78ef02d398a"
|
|
12911
12921
|
},
|
|
12912
12922
|
{
|
|
12913
12923
|
"path": "tests/criterion_ref_status.mjs",
|
package/docs/copy-inventory.md
CHANGED
|
@@ -737,7 +737,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
737
737
|
| `de1713edd4aa` | certain | Next page | `modules/hall-ui/public/hall-kit.js:74` |
|
|
738
738
|
| `659d8256139c` | certain | Next up — ready to claim | `modules/hall-ui/public/hall-widgets.js:103` |
|
|
739
739
|
| `f52ba261d930` | certain | Next › | `modules/hall-ui/public/hall-kit.js:74` |
|
|
740
|
-
| `bef2cdebc5c6` | certain | No AI sessions | `modules/hall-ui/public/roster.js:
|
|
740
|
+
| `bef2cdebc5c6` | certain | No AI sessions | `modules/hall-ui/public/roster.js:231` |
|
|
741
741
|
| `41ec1cf65d6f` | certain | No GitHub account yet? Make a free one at | `modules/hall-ui/public/hall-render.js:297` |
|
|
742
742
|
| `c255749ec989` | certain | No achievements earned yet. | `modules/hall-ui/public/builders.js:470` |
|
|
743
743
|
| `c255749ec989` | certain | No achievements earned yet. | `modules/hall-ui/public/profile.js:199` |
|
|
@@ -1841,7 +1841,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1841
1841
|
| `a072071b893c` | certain | All time | `modules/status-ui/public/index.html:313` |
|
|
1842
1842
|
| `1fb3f6a598cd` | certain | Architects of {{worldName}} | `modules/status-ui/public/index.html:200` |
|
|
1843
1843
|
| `4b16eed738cf` | certain | Attributed to builders: {…} · Shared keep (unattributed): {…} ({…}%). | `modules/status-ui/public/status.js:718` |
|
|
1844
|
-
| `20a83501bba6` | certain | Avg time per work (min) | `modules/status-ui/public/status.js:
|
|
1844
|
+
| `20a83501bba6` | certain | Avg time per work (min) | `modules/status-ui/public/status.js:1258` |
|
|
1845
1845
|
| `c8959bdc0e59` | certain | Breakdown filtered to {…}: {…} of {…} ({…}% of all spend). | `modules/status-ui/public/status.js:659` |
|
|
1846
1846
|
| `1fe8937399f5` | certain | By order of the Senate | `modules/status-ui/public/index.html:354` |
|
|
1847
1847
|
| `21f644463d57` | certain | Citizens who have set the most stone. | `modules/status-ui/public/index.html:201` |
|
|
@@ -1867,7 +1867,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1867
1867
|
| `dc9eab0a02e2` | certain | Newest entries set down upon the ledger. | `modules/status-ui/public/index.html:305` |
|
|
1868
1868
|
| `9ef3aaa7b441` | certain | Next page | `modules/status-ui/public/index.html:323` |
|
|
1869
1869
|
| `37b171a6195a` | certain | No architects yet — be the first to lay a stone. | `modules/status-ui/public/index.html:205` |
|
|
1870
|
-
| `7816fbe0add8` | certain | No builders yet — be the first. | `modules/status-ui/public/status.js:
|
|
1870
|
+
| `7816fbe0add8` | certain | No builders yet — be the first. | `modules/status-ui/public/status.js:1150` |
|
|
1871
1871
|
| `0b097f051d6f` | certain | No endeavors underway just now. | `modules/status-ui/public/status.js:516` |
|
|
1872
1872
|
| `ed9b06f79234` | certain | No projects on the map yet — check back soon. | `modules/status-ui/public/explore.html:108` |
|
|
1873
1873
|
| `4b1a11bfdf10` | certain | No works yet inscribed in this window. | `modules/status-ui/public/status.js:947` |
|
|
@@ -1899,7 +1899,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1899
1899
|
| `d1add9aa4db0` | certain | The standing of each pillar — whether it holds or has fallen. | `modules/status-ui/public/index.html:188` |
|
|
1900
1900
|
| `c1ab82f376ec` | certain | The work, in brief | `modules/status-ui/public/status.js:594` |
|
|
1901
1901
|
| `6cc811f31b3a` | certain | Works in Progress | `modules/status-ui/public/index.html:231` |
|
|
1902
|
-
| `4abea0f631eb` | certain | Works shipped / day | `modules/status-ui/public/status.js:
|
|
1902
|
+
| `4abea0f631eb` | certain | Works shipped / day | `modules/status-ui/public/status.js:1257` |
|
|
1903
1903
|
| `b2f58817e34e` | certain | World snapshots over time | `modules/status-ui/public/index.html:343` |
|
|
1904
1904
|
| `ebdc5ecf315c` | certain | avg time / work | `modules/status-ui/public/index.html:220` |
|
|
1905
1905
|
| `774e102fc137` | certain | city's standing | `modules/status-ui/public/index.html:163` |
|
|
@@ -1912,7 +1912,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1912
1912
|
| `89ef611c7d37` | certain | page 1 of 1 | `modules/status-ui/public/index.html:322` |
|
|
1913
1913
|
| `4a00bdbfd9be` | certain | page {…} of {…} | `modules/status-ui/public/status.js:1012` |
|
|
1914
1914
|
| `1d0fbe3e5927` | certain | passed the inspector, last 7 days | `modules/status-ui/public/index.html:178` |
|
|
1915
|
-
| `c1e3c2212207` | certain | tablet last consulted at {…} | `modules/status-ui/public/status.js:
|
|
1915
|
+
| `c1e3c2212207` | certain | tablet last consulted at {…} | `modules/status-ui/public/status.js:1202` |
|
|
1916
1916
|
| `fefb0130c7ae` | certain | value wrought | `modules/status-ui/public/index.html:255` |
|
|
1917
1917
|
| `b97f429c33fe` | certain | works shipped (90d) | `modules/status-ui/public/index.html:218` |
|
|
1918
1918
|
| `71a303e1ba09` | certain | works wrought, last 7 days | `modules/status-ui/public/index.html:167` |
|
package/docs/copy-registry.json
CHANGED
|
@@ -8338,7 +8338,7 @@
|
|
|
8338
8338
|
"surface": "builders-hall",
|
|
8339
8339
|
"text": "No AI sessions",
|
|
8340
8340
|
"file": "modules/hall-ui/public/roster.js",
|
|
8341
|
-
"line":
|
|
8341
|
+
"line": 231,
|
|
8342
8342
|
"origin": "js-markup",
|
|
8343
8343
|
"confidence": "certain"
|
|
8344
8344
|
},
|
|
@@ -24502,7 +24502,7 @@
|
|
|
24502
24502
|
"surface": "status-dashboard",
|
|
24503
24503
|
"text": "Avg time per work (min)",
|
|
24504
24504
|
"file": "modules/status-ui/public/status.js",
|
|
24505
|
-
"line":
|
|
24505
|
+
"line": 1258,
|
|
24506
24506
|
"origin": "js-markup",
|
|
24507
24507
|
"confidence": "certain"
|
|
24508
24508
|
},
|
|
@@ -24736,7 +24736,7 @@
|
|
|
24736
24736
|
"surface": "status-dashboard",
|
|
24737
24737
|
"text": "No builders yet — be the first.",
|
|
24738
24738
|
"file": "modules/status-ui/public/status.js",
|
|
24739
|
-
"line":
|
|
24739
|
+
"line": 1150,
|
|
24740
24740
|
"origin": "js-text-assign",
|
|
24741
24741
|
"confidence": "certain"
|
|
24742
24742
|
},
|
|
@@ -25024,7 +25024,7 @@
|
|
|
25024
25024
|
"surface": "status-dashboard",
|
|
25025
25025
|
"text": "Works shipped / day",
|
|
25026
25026
|
"file": "modules/status-ui/public/status.js",
|
|
25027
|
-
"line":
|
|
25027
|
+
"line": 1257,
|
|
25028
25028
|
"origin": "js-markup",
|
|
25029
25029
|
"confidence": "certain"
|
|
25030
25030
|
},
|
|
@@ -25141,7 +25141,7 @@
|
|
|
25141
25141
|
"surface": "status-dashboard",
|
|
25142
25142
|
"text": "tablet last consulted at {…}",
|
|
25143
25143
|
"file": "modules/status-ui/public/status.js",
|
|
25144
|
-
"line":
|
|
25144
|
+
"line": 1202,
|
|
25145
25145
|
"origin": "js-text-assign",
|
|
25146
25146
|
"confidence": "certain"
|
|
25147
25147
|
},
|
|
@@ -2831,5 +2831,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2831
2831
|
landed since 1.21.0 with no explicit bump. run 36940540656. (task 1002620)
|
|
2832
2832
|
1.21.2 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2833
2833
|
landed since 1.21.1 with no explicit bump. run 36943290103. (task 1002620)
|
|
2834
|
+
1.21.3 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2835
|
+
landed since 1.21.2 with no explicit bump. run 36944637655. (task 1002620)
|
|
2836
|
+
1.21.4 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2837
|
+
landed since 1.21.3 with no explicit bump. run 36954271746. (task 1002620)
|
|
2834
2838
|
---------------------------------------------------------------------------
|
|
2835
2839
|
```
|
package/docs/page-readings.json
CHANGED
|
@@ -2491,7 +2491,7 @@
|
|
|
2491
2491
|
"surface": "builders",
|
|
2492
2492
|
"title": "Builders",
|
|
2493
2493
|
"states_read": ["roster","artists","search","out"],
|
|
2494
|
-
"files_hash": "
|
|
2494
|
+
"files_hash": "5da729281e697115",
|
|
2495
2495
|
"reading_hash": "1cf32981ae9e428e",
|
|
2496
2496
|
"counts": {"lines":36,"placed":8,"shared":5,"unplaced":23},
|
|
2497
2497
|
"omitted": {"data":58},
|
|
@@ -5035,7 +5035,7 @@
|
|
|
5035
5035
|
"surface": "status",
|
|
5036
5036
|
"title": "{{worldName}} — the public ledger",
|
|
5037
5037
|
"states_read": ["default"],
|
|
5038
|
-
"files_hash": "
|
|
5038
|
+
"files_hash": "d825f08b6d62d51e",
|
|
5039
5039
|
"reading_hash": "2853e52b66ba076c",
|
|
5040
5040
|
"counts": {"lines":87,"placed":49,"shared":14,"unplaced":24},
|
|
5041
5041
|
"omitted": {"data":89},
|
|
@@ -5044,7 +5044,7 @@
|
|
|
5044
5044
|
{"key":"L0001","section":"top bar","text":"The Public Ledger of the City's Making","placement":"shared","file":"modules/status-ui/public/index.html","line":125,"string_id":"acdf97805931"},
|
|
5045
5045
|
{"key":"L0002","section":"top bar","text":"A shipping container has washed ashore upon the coast south of Athens. About it a new civilization rises. Set out below: the city's affairs, openly recorded.","placement":"shared","file":"modules/status-ui/public/index.html","line":126,"string_id":"f4a664041da5"},
|
|
5046
5046
|
{"key":"L0003","section":"top bar","text":"Cross into the city →","placement":"shared","file":"modules/status-ui/public/index.html","line":128,"string_id":"3aa071ef1142"},
|
|
5047
|
-
{"key":"L0004","section":"top bar","text":"tablet last consulted at {…}","placement":"shared","file":"modules/status-ui/public/status.js","line":
|
|
5047
|
+
{"key":"L0004","section":"top bar","text":"tablet last consulted at {…}","placement":"shared","file":"modules/status-ui/public/status.js","line":1202,"string_id":"c1e3c2212207"},
|
|
5048
5048
|
{"key":"L0005","section":"navigation","text":"Pulse","placement":"shared"},
|
|
5049
5049
|
{"key":"L0006","section":"navigation","text":"Watch","placement":"shared"},
|
|
5050
5050
|
{"key":"L0007","section":"navigation","text":"Architects","placement":"shared"},
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- core_271_bongos_shipper_badge_name.sql — the shipper badge carries the platform's
|
|
2
|
+
-- name (task 1004532).
|
|
3
|
+
--
|
|
4
|
+
-- The badge for shipping work on the platform itself was renamed once already, from
|
|
5
|
+
-- the first name to "GDS Shipper" (migration 019), and the platform has since been
|
|
6
|
+
-- renamed Bongos (goal 1000073). The roster and the status page now show each
|
|
7
|
+
-- badge's catalog NAME on hover, so the old name would be spelled out to every
|
|
8
|
+
-- reader. The id `gds-shipper` stays: builder_achievements rows key on it, and an
|
|
9
|
+
-- id is not a word a reader sees.
|
|
10
|
+
|
|
11
|
+
UPDATE achievements
|
|
12
|
+
SET name = 'Bongos Shipper',
|
|
13
|
+
description = 'Ship a task whose touches[] include src/bongos/ or scripts/gds/.'
|
|
14
|
+
WHERE id = 'gds-shipper';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
-- core_272 — rename goal 1000045's criteria off the positional shape (task 1003122)
|
|
2
|
+
--
|
|
3
|
+
-- parseCriterionRef reads a ref shaped C1, C2... as a POSITION before it ever
|
|
4
|
+
-- consults slugs, so a criterion whose criterion_id is literally 'C5' cannot be
|
|
5
|
+
-- named by its own slug (and on a multi-goal version the ref is refused as
|
|
6
|
+
-- ambiguous). Goal 1000045 slugged ten criteria C1..C10, and a seeder passing 'C5'
|
|
7
|
+
-- meaning that goal's C5 got another goal's criterion. createCriterion now refuses
|
|
8
|
+
-- the shape; this renames the ten that already exist, once, owner-approved names.
|
|
9
|
+
--
|
|
10
|
+
-- Links are by done_when_criteria.id (task_criteria.criterion_id), so no task link
|
|
11
|
+
-- moves. Scoped by goal AND old slug, so it is idempotent and touches nothing on an
|
|
12
|
+
-- instance that never had this goal. Data only: forward-safe.
|
|
13
|
+
--
|
|
14
|
+
-- WHY core/ AND NOT migrations/instance/ (ADR 0105). That directory does not exist
|
|
15
|
+
-- in the portable core, and the instance seed path only runs under the owner opt-in
|
|
16
|
+
-- GDS_APPLY_INSTANCE_SEEDS=1 (a DR step), so a rename placed there would never run
|
|
17
|
+
-- on the live instance that needs it. The cost is a no-op UPDATE on every other
|
|
18
|
+
-- host; the double guard (this goal id AND the exact old slug AND the new slug free)
|
|
19
|
+
-- keeps it from touching an unrelated goal that happens to share the id.
|
|
20
|
+
|
|
21
|
+
UPDATE done_when_criteria c
|
|
22
|
+
SET criterion_id = m.new_slug
|
|
23
|
+
FROM (VALUES
|
|
24
|
+
('C1', 'bp-public-cross-project-record'),
|
|
25
|
+
('C2', 'bp-hide-my-record'),
|
|
26
|
+
('C3', 'bp-apply-from-orbs'),
|
|
27
|
+
('C4', 'bp-application-carries-profile'),
|
|
28
|
+
('C5', 'bp-owner-browses-builders'),
|
|
29
|
+
('C6', 'bp-builder-connections'),
|
|
30
|
+
('C7', 'bp-guilds'),
|
|
31
|
+
('C8', 'bp-guild-apply-and-invite'),
|
|
32
|
+
('C9', 'bp-guild-map'),
|
|
33
|
+
('C10', 'bp-connection-graph-to-guild')
|
|
34
|
+
) AS m(old_slug, new_slug)
|
|
35
|
+
WHERE c.goal_id = 1000045
|
|
36
|
+
AND c.criterion_id = m.old_slug
|
|
37
|
+
AND NOT EXISTS (
|
|
38
|
+
SELECT 1 FROM done_when_criteria d
|
|
39
|
+
WHERE d.version_id = c.version_id AND d.criterion_id = m.new_slug
|
|
40
|
+
);
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// marked `private` still appears on the roster of a project they build on
|
|
15
15
|
// (owner, 2026-08-30; ADR 0225, task 1003443).
|
|
16
16
|
//
|
|
17
|
-
// The laurels (
|
|
17
|
+
// The laurels (ACH + achRowHtml) and the sort lenses MOVED here from
|
|
18
18
|
// hall-render.js rather than being copied — the leaderboard row was their only
|
|
19
19
|
// caller, so the home page is left carrying no dead renderer.
|
|
20
20
|
(function () {
|
|
@@ -43,37 +43,36 @@
|
|
|
43
43
|
return '<span class="rank-badge ' + variant + '" aria-label="' + esc(aria) + '">' + esc(rankLabel(rank)) + '</span>';
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
// Each laurel's emoji AND its catalog name (the achievements table's `name`,
|
|
47
|
+
// migrations 006/019/031/core_270), so a hover and a screen reader say what the
|
|
48
|
+
// badge is called (task 1004532) — never the id, and never words derived from it,
|
|
49
|
+
// which disagree with the catalog the moment a badge is renamed.
|
|
50
|
+
// tests/craft_achievements.mjs holds every row to the catalog.
|
|
51
|
+
var ACH = {
|
|
52
|
+
'first-ship': ['\u{1F6A2}', 'First Ship'],
|
|
53
|
+
'first-claim': ['\u{1FA99}', 'First Claim'],
|
|
54
|
+
'first-parallel-claim': ['⚓', 'First Parallel Claim'],
|
|
55
|
+
'streak-3': ['\u{1F525}', 'Streak: 3 Nights'],
|
|
56
|
+
'streak-7': ['\u{1F30B}', 'Streak: 7 Nights'],
|
|
52
57
|
// task 1004438: renamed Art Pipeline Engineer; the palette is the artist's now
|
|
53
|
-
'art-pipeline-shipper': '\u{1F6E0}\u{FE0F}',
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'thought-
|
|
58
|
-
'first-
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
'
|
|
58
|
+
'art-pipeline-shipper': ['\u{1F6E0}\u{FE0F}', 'Art Pipeline Engineer'],
|
|
59
|
+
'subtractor': ['\u{1FA93}', 'Subtractor'],
|
|
60
|
+
'gds-shipper': ['⚙️', 'Bongos Shipper'],
|
|
61
|
+
'breaking-the-100c-bar': ['\u{1F4AF}', 'Breaking the 100c Bar'],
|
|
62
|
+
'first-thought-ignited': ['\u{1F4A1}', 'First Thought Ignited'],
|
|
63
|
+
'first-full-idea': ['\u{1F4DC}', 'First Full Idea'],
|
|
64
|
+
'first-board-ratification': ['\u{1F3DB}\u{FE0F}', 'Ratified by the Board'],
|
|
65
|
+
'thought-built-ten': ['\u{1F31F}', 'A Thought That Built Ten'],
|
|
66
|
+
'first-page-approved': ['\u{1F3A8}', 'First Page Approved'],
|
|
67
|
+
'ten-pages-approved': ['\u{1F5BC}\u{FE0F}', 'Ten Pages Approved'],
|
|
68
|
+
'whole-surface-tweaked': ['\u{1F5FA}\u{FE0F}', 'A Whole Surface'],
|
|
63
69
|
};
|
|
64
|
-
// An achievement id as a sentence: "first-page-approved" → "First page approved".
|
|
65
|
-
function achWords(id) {
|
|
66
|
-
var s = String(id || '').replace(/-/g, ' ');
|
|
67
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
68
|
-
}
|
|
69
70
|
function achRowHtml(ids) {
|
|
70
71
|
if (!Array.isArray(ids) || !ids.length) return '';
|
|
71
72
|
var inner = ids.map(function (id) {
|
|
72
|
-
var
|
|
73
|
-
// The title is the
|
|
74
|
-
|
|
75
|
-
var words = achWords(id);
|
|
76
|
-
return emoji ? '<span class="lb-ach" title="' + esc(words) + '" aria-label="' + esc(words) + '">' + emoji + '</span>' : '';
|
|
73
|
+
var a = ACH[id];
|
|
74
|
+
// The title is the badge's catalog name: what a hover shows and a screen reader says.
|
|
75
|
+
return a ? '<span class="lb-ach" title="' + esc(a[1]) + '" aria-label="' + esc(a[1]) + '">' + a[0] + '</span>' : '';
|
|
77
76
|
}).join('');
|
|
78
77
|
return inner ? '<span class="lb-ach-row" aria-label="achievements">' + inner + '</span>' : '';
|
|
79
78
|
}
|
|
@@ -46,6 +46,16 @@ function parseCriterionRef(raw) {
|
|
|
46
46
|
return { kind: 'slug', slug: ref };
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// A criterion_id slug may not be shaped like a positional token (task 1003122).
|
|
50
|
+
// parseCriterionRef reads 'C5' as a position BEFORE it ever consults slugs, so a
|
|
51
|
+
// criterion whose slug is literally 'C5' is unreachable by its own name — and on a
|
|
52
|
+
// multi-goal version the ref is refused as ambiguous. Goal 1000045 slugged ten
|
|
53
|
+
// criteria 'C1'..'C10' and a seeder's 'C5' landed on another goal's criterion.
|
|
54
|
+
// Refuse the shape at write time so the ambiguity cannot exist (option b).
|
|
55
|
+
function isReservedCriterionSlug(raw) {
|
|
56
|
+
return parseCriterionRef(raw).kind === 'cnum';
|
|
57
|
+
}
|
|
58
|
+
|
|
49
59
|
// A version id as this project writes them: an uppercase prefix (possibly none)
|
|
50
60
|
// followed by V + digits — V1, PMS-V2, GDS-V4, BONGOS-V2, CB-V1.
|
|
51
61
|
//
|
|
@@ -101,6 +111,7 @@ function criterionLabel(c) {
|
|
|
101
111
|
|
|
102
112
|
module.exports = {
|
|
103
113
|
parseCriterionRef,
|
|
114
|
+
isReservedCriterionSlug,
|
|
104
115
|
looksLikeVersionId,
|
|
105
116
|
resolveCriterionIn,
|
|
106
117
|
criterionLabel,
|
|
@@ -18,6 +18,7 @@ const { matchProtected } = api.permissionPathCheck;
|
|
|
18
18
|
const { decorateGoalMembership, capNewcomerTasks } = require('./claim-feed.js');
|
|
19
19
|
const { deriveRequiredRank, highestRank } = require('./db-rank-authz.js');
|
|
20
20
|
const { executorFor } = require('./db-executor.js');
|
|
21
|
+
const { isReservedCriterionSlug } = require('./criterion-ref.js');
|
|
21
22
|
const { resolveWriteDiscipline, WRITABLE_DISCIPLINES } = require('./task-classifier.js');
|
|
22
23
|
|
|
23
24
|
// -------------------------------------------------------------------------
|
|
@@ -366,6 +367,12 @@ async function updateTaskColumn(id, column, value) {
|
|
|
366
367
|
// it are one decision, and splitting them across connections reopens a narrower
|
|
367
368
|
// version of the same race — two creates reading the same next order.
|
|
368
369
|
async function createCriterion({ versionId, criterionId, criterionMd, goalId = null, sortOrder = null }, deps = {}) {
|
|
370
|
+
if (isReservedCriterionSlug(criterionId)) {
|
|
371
|
+
// Task 1003122: a 'Cn' slug is unreachable by name (the positional reading wins).
|
|
372
|
+
const err = new Error(`criterion_id '${criterionId}' is shaped like a positional ref (C1, C2, ...); choose a descriptive slug.`);
|
|
373
|
+
err.code = 'RESERVED_CRITERION_SLUG';
|
|
374
|
+
throw err;
|
|
375
|
+
}
|
|
369
376
|
const executor = executorFor(deps, pool);
|
|
370
377
|
let order = sortOrder;
|
|
371
378
|
if (order == null) {
|
|
@@ -23,6 +23,7 @@ const { isGoalOwnerOrManager } = require('../goal-authz');
|
|
|
23
23
|
const goalEdits = require('../goal-edits.js');
|
|
24
24
|
// task 1004392 — /satisfy is the override now and records itself as one.
|
|
25
25
|
const uatDb = require('../criterion-uat-db.js');
|
|
26
|
+
const { isReservedCriterionSlug } = require('../criterion-ref.js');
|
|
26
27
|
const { validateOrRespond, parseId, asyncHandler, LIMITS } = api;
|
|
27
28
|
|
|
28
29
|
// authorizeCriterionCreate — the ORDERED, FAIL-CLOSED decision for a non-Archon
|
|
@@ -132,6 +133,11 @@ module.exports = function buildDoneWhenRouter() {
|
|
|
132
133
|
goal_id: { type: 'integer', min: 1 },
|
|
133
134
|
sort_order: { type: 'integer', min: 0 },
|
|
134
135
|
})) return;
|
|
136
|
+
// Task 1003122: refuse before the transaction takes its locks. createCriterion
|
|
137
|
+
// enforces the same rule for every other caller (the ratified-goal path).
|
|
138
|
+
if (isReservedCriterionSlug(req.body.criterion_id)) {
|
|
139
|
+
return res.fail('reserved_criterion_slug', { status: 400, message: 'criterion_id may not be shaped like a positional ref (C1, C2, ...); choose a descriptive slug.' });
|
|
140
|
+
}
|
|
135
141
|
// bad_goal_id now means exactly what it says: the goal is absent or belongs
|
|
136
142
|
// to another version. (The unauthorized case rendered through this same
|
|
137
143
|
// helper until task 1002614 — see the mask note in authorizeCriterionCreate.)
|
|
@@ -1077,27 +1077,30 @@ function updateHeadlineShippedFromItems() {
|
|
|
1077
1077
|
// ---------- leaderboard ----------
|
|
1078
1078
|
|
|
1079
1079
|
// The catalog is small and changes only by migration (the latest: the seven
|
|
1080
|
-
// craft achievements of task 1004438, migrations/core_270), so hardcoding
|
|
1081
|
-
//
|
|
1082
|
-
//
|
|
1083
|
-
//
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
'first-
|
|
1088
|
-
'
|
|
1089
|
-
'
|
|
1080
|
+
// craft achievements of task 1004438, migrations/core_270), so hardcoding it here
|
|
1081
|
+
// is cheaper than another API round-trip per page load. Each laurel carries its
|
|
1082
|
+
// emoji AND its catalog name (task 1004532), so a hover and a screen reader say
|
|
1083
|
+
// what the badge is called — never the id, and never words derived from it.
|
|
1084
|
+
// Mirrors modules/hall-ui/public/roster.js ACH — tests/craft_achievements.mjs
|
|
1085
|
+
// holds both maps to the catalog's ids, icons and names.
|
|
1086
|
+
const ACH = {
|
|
1087
|
+
'first-ship': ['🚢', 'First Ship'],
|
|
1088
|
+
'first-claim': ['🪙', 'First Claim'],
|
|
1089
|
+
'first-parallel-claim': ['⚓', 'First Parallel Claim'],
|
|
1090
|
+
'streak-3': ['🔥', 'Streak: 3 Nights'],
|
|
1091
|
+
'streak-7': ['🌋', 'Streak: 7 Nights'],
|
|
1090
1092
|
// task 1004438: renamed Art Pipeline Engineer; the palette is the artist's now
|
|
1091
|
-
'art-pipeline-shipper': '🛠️',
|
|
1092
|
-
'
|
|
1093
|
-
'
|
|
1094
|
-
'
|
|
1095
|
-
'thought-
|
|
1096
|
-
'first-
|
|
1097
|
-
'
|
|
1098
|
-
'
|
|
1099
|
-
'
|
|
1100
|
-
'
|
|
1093
|
+
'art-pipeline-shipper': ['🛠️', 'Art Pipeline Engineer'],
|
|
1094
|
+
'subtractor': ['🪓', 'Subtractor'],
|
|
1095
|
+
'gds-shipper': ['⚙️', 'Bongos Shipper'],
|
|
1096
|
+
'breaking-the-100c-bar': ['💯', 'Breaking the 100c Bar'],
|
|
1097
|
+
'first-thought-ignited': ['💡', 'First Thought Ignited'],
|
|
1098
|
+
'first-full-idea': ['📜', 'First Full Idea'],
|
|
1099
|
+
'first-board-ratification': ['🏛️', 'Ratified by the Board'],
|
|
1100
|
+
'thought-built-ten': ['🌟', 'A Thought That Built Ten'],
|
|
1101
|
+
'first-page-approved': ['🎨', 'First Page Approved'],
|
|
1102
|
+
'ten-pages-approved': ['🖼️', 'Ten Pages Approved'],
|
|
1103
|
+
'whole-surface-tweaked': ['🗺️', 'A Whole Surface'],
|
|
1101
1104
|
};
|
|
1102
1105
|
|
|
1103
1106
|
function renderAchievementRow(ids) {
|
|
@@ -1106,15 +1109,13 @@ function renderAchievementRow(ids) {
|
|
|
1106
1109
|
row.className = 'leader__achievements';
|
|
1107
1110
|
row.setAttribute('aria-label', 'achievements');
|
|
1108
1111
|
for (const id of ids) {
|
|
1109
|
-
const
|
|
1110
|
-
if (!
|
|
1112
|
+
const a = ACH[id];
|
|
1113
|
+
if (!a) continue;
|
|
1111
1114
|
const span = document.createElement('span');
|
|
1112
1115
|
span.className = 'leader__ach';
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
span.
|
|
1116
|
-
span.setAttribute('aria-label', span.title);
|
|
1117
|
-
span.textContent = emoji;
|
|
1116
|
+
span.title = a[1];
|
|
1117
|
+
span.setAttribute('aria-label', a[1]);
|
|
1118
|
+
span.textContent = a[0];
|
|
1118
1119
|
row.appendChild(span);
|
|
1119
1120
|
}
|
|
1120
1121
|
return row.children.length > 0 ? row : null;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.4",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.4",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -8704,5 +8704,17 @@
|
|
|
8704
8704
|
"id": "1004438",
|
|
8705
8705
|
"text": "Artists and ideators now earn achievements for their own work, not only for shipping code. Ideators: First Thought Ignited, First Full Idea, Ratified by the Board, and A Thought That Built Ten. Artists: First Page Approved, Te"
|
|
8706
8706
|
}
|
|
8707
|
+
],
|
|
8708
|
+
"1.21.3": [
|
|
8709
|
+
{
|
|
8710
|
+
"id": "1004532",
|
|
8711
|
+
"text": "Hovering over a badge on the builders list or the public status page now shows its real name, such as \"Art Pipeline Engineer\" or \"A Thought That Built Ten\", instead of words made from its internal code. Screen readers read the"
|
|
8712
|
+
}
|
|
8713
|
+
],
|
|
8714
|
+
"1.21.4": [
|
|
8715
|
+
{
|
|
8716
|
+
"id": "1003122",
|
|
8717
|
+
"text": "New criteria can no longer be named like C5, which the system always read as a position and so could never find by name."
|
|
8718
|
+
}
|
|
8707
8719
|
]
|
|
8708
8720
|
}
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.4'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -319,31 +319,70 @@ await test('the backfill keys each badge on the same ledger fact and stamps the
|
|
|
319
319
|
|
|
320
320
|
// ---- 5. the pictures ------------------------------------------------------------------
|
|
321
321
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
322
|
+
// The catalog as the database holds it: every migration's achievements rows and
|
|
323
|
+
// renames, applied in filename order (the order the migrator runs them).
|
|
324
|
+
function dbCatalog() {
|
|
325
|
+
const dir = path.join(ROOT, 'migrations');
|
|
326
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.sql')).sort();
|
|
327
|
+
const econ = path.join(ROOT, 'modules/economy/migrations');
|
|
328
|
+
const all = [...files.map((f) => path.join(dir, f)), ...fs.readdirSync(econ).filter((f) => f.endsWith('.sql')).sort().map((f) => path.join(econ, f))];
|
|
329
|
+
const cat = new Map();
|
|
330
|
+
for (const file of all) {
|
|
331
|
+
const sql = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
|
|
332
|
+
for (const block of sql.split(/INSERT INTO achievements\b/).slice(1)) {
|
|
333
|
+
const values = block.split(/ON CONFLICT|;\s*\n/)[0];
|
|
334
|
+
for (const m of values.matchAll(/\('([a-z0-9-]+)',\s*'([^']+)',\s*'(?:[^']|'')*',\s*'([^']+)'/g)) {
|
|
335
|
+
cat.set(m[1], { name: m[2], icon: m[3] });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
for (const m of sql.matchAll(/UPDATE achievements\s+SET ([\s\S]*?)\s+WHERE id = '([a-z0-9-]+)';/g)) {
|
|
339
|
+
const [, sets, where] = m;
|
|
340
|
+
const row = { ...(cat.get(where) || {}) };
|
|
341
|
+
const name = sets.match(/name\s*=\s*'([^']+)'/);
|
|
342
|
+
const icon = sets.match(/icon\s*=\s*'([^']+)'/);
|
|
343
|
+
const id = sets.match(/\bid\s*=\s*'([a-z0-9-]+)'/);
|
|
344
|
+
if (name) row.name = name[1];
|
|
345
|
+
if (icon) row.icon = icon[1];
|
|
346
|
+
if (id) { cat.delete(where); cat.set(id[1], row); } else cat.set(where, row);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return cat;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function mapOf(src, constName) {
|
|
353
|
+
const body = src.slice(src.indexOf(`${constName} = {`), src.indexOf('};', src.indexOf(`${constName} = {`)));
|
|
325
354
|
const unescape = (s) => s.replace(/\\u\{([0-9A-F]+)\}/gi, (_, h) => String.fromCodePoint(parseInt(h, 16)));
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
355
|
+
const out = new Map();
|
|
356
|
+
for (const m of body.matchAll(/'([a-z0-9-]+)': \['([^']+)', '([^']+)'\]/g)) out.set(m[1], { icon: unescape(m[2]), name: m[3] });
|
|
357
|
+
return out;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
await test('the roster and the status page draw every badge with its catalog icon AND its catalog name', () => {
|
|
361
|
+
const cat = dbCatalog();
|
|
362
|
+
assert.equal(cat.get('art-pipeline-shipper').name, 'Art Pipeline Engineer', 'the catalog parse applies the rename');
|
|
363
|
+
assert.equal(cat.get('gds-shipper').name, 'Bongos Shipper', 'and the id rename (019) and the platform rename (core_271)');
|
|
364
|
+
for (const [file, constName] of [['modules/hall-ui/public/roster.js', 'var ACH'], ['modules/status-ui/public/status.js', 'const ACH']]) {
|
|
365
|
+
const map = mapOf(read(file), constName);
|
|
366
|
+
assert.deepEqual([...map.keys()].sort(), [...cat.keys()].sort(), `${file} names every badge in the catalog`);
|
|
367
|
+
for (const [id, row] of cat) {
|
|
368
|
+
assert.equal(map.get(id).name, row.name, `${file}: ${id} is named "${row.name}"`);
|
|
369
|
+
// Icons are pinned for the badges core_270 drew or redrew; the older laurels'
|
|
370
|
+
// list glyphs predate this (first-ship has always been a ship here, a box on
|
|
371
|
+
// the shelf) and are not this task's to change.
|
|
372
|
+
if (CRAFT.includes(id) || id === 'art-pipeline-shipper') {
|
|
373
|
+
assert.equal(map.get(id).icon, row.icon, `${file}: ${id} is drawn as ${row.icon}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
331
376
|
}
|
|
332
|
-
assert.ok(unescape(roster.match(/'art-pipeline-shipper': '([^']+)'/)[1]) === '🛠️');
|
|
333
|
-
assert.equal(status.match(/'art-pipeline-shipper': '([^']+)'/)[1], '🛠️');
|
|
334
377
|
});
|
|
335
378
|
|
|
336
|
-
await test('a
|
|
379
|
+
await test('a hover and a screen reader say the badge\'s name, never its id or words made from it', () => {
|
|
337
380
|
const roster = read('modules/hall-ui/public/roster.js');
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
assert.equal(achWords('first-page-approved'), 'First page approved');
|
|
341
|
-
assert.equal(achWords('thought-built-ten'), 'Thought built ten');
|
|
342
|
-
assert.match(roster, /title="' \+ esc\(words\) \+ '" aria-label="' \+ esc\(words\) \+ '"/);
|
|
343
|
-
assert.doesNotMatch(roster, /title="' \+ esc\(id\)/);
|
|
381
|
+
assert.match(roster, /title="' \+ esc\(a\[1\]\) \+ '" aria-label="' \+ esc\(a\[1\]\) \+ '"/);
|
|
382
|
+
assert.doesNotMatch(roster, /achWords|esc\(id\)/);
|
|
344
383
|
const status = read('modules/status-ui/public/status.js');
|
|
345
|
-
assert.
|
|
346
|
-
assert.
|
|
384
|
+
assert.match(status, /span\.title = a\[1\];\s*span\.setAttribute\('aria-label', a\[1\]\);/);
|
|
385
|
+
assert.doesNotMatch(status, /span\.title = id|replace\(\/-\/g, ' '\)/);
|
|
347
386
|
});
|
|
348
387
|
|
|
349
388
|
summary();
|
|
@@ -487,7 +487,7 @@ await test('FOR UPDATE outside a transaction is refused, not silently downgraded
|
|
|
487
487
|
await test('CREATE: the goal is locked, inside the transaction (task 1003840)', async () => {
|
|
488
488
|
actor = ARCHON;
|
|
489
489
|
goalReads.length = 0;
|
|
490
|
-
const r = await post(V1, { criterion_id: '
|
|
490
|
+
const r = await post(V1, { criterion_id: 'brand-new-slug', criterion_md: 'x', goal_id: 3 });
|
|
491
491
|
assert.equal(r.status, 201, JSON.stringify(r.body));
|
|
492
492
|
assert.ok(goalReads.length > 0, 'the route read the goal');
|
|
493
493
|
for (const g of goalReads) {
|
|
@@ -498,7 +498,7 @@ await test('CREATE: the goal is locked, inside the transaction (task 1003840)',
|
|
|
498
498
|
|
|
499
499
|
await test('CREATE: the INSERT runs ON the transaction client (task 1003840)', async () => {
|
|
500
500
|
actor = ARCHON;
|
|
501
|
-
await post(V1, { criterion_id: '
|
|
501
|
+
await post(V1, { criterion_id: 'brand-new-slug', criterion_md: 'x', goal_id: 3 });
|
|
502
502
|
const inserts = sqlLog.filter((q) => /INSERT INTO done_when_criteria/.test(q.sql));
|
|
503
503
|
assert.equal(inserts.length, 1, 'exactly one INSERT');
|
|
504
504
|
assert.equal(inserts[0].via, 'client', 'the INSERT must not leave the transaction');
|
|
@@ -512,7 +512,7 @@ await test('CREATE: the INSERT runs ON the transaction client (task 1003840)', a
|
|
|
512
512
|
await test('CREATE: a refusal writes nothing and releases the client (task 1003840)', async () => {
|
|
513
513
|
actor = METIC;
|
|
514
514
|
goals.set(3, { id: 3, version_id: V1, owner_builder_id: 999, status: 'open' });
|
|
515
|
-
const r = await post(V1, { criterion_id: '
|
|
515
|
+
const r = await post(V1, { criterion_id: 'brand-new-slug', criterion_md: 'x', goal_id: 3 });
|
|
516
516
|
assert.ok(r.status >= 400, `expected a refusal, got ${r.status}`);
|
|
517
517
|
assert.equal(sqlLog.filter((q) => /INSERT INTO done_when_criteria/.test(q.sql)).length, 0, 'nothing was inserted');
|
|
518
518
|
assert.ok(clients.length > 0 && clients.every((c) => c.released), 'the client is released on the refusal path');
|
|
@@ -524,7 +524,7 @@ await test('the VERSION is locked too, not only the goal (task 1003840)', async
|
|
|
524
524
|
// version could still be frozen between this read and this COMMIT.
|
|
525
525
|
actor = ARCHON;
|
|
526
526
|
versionReads.length = 0;
|
|
527
|
-
await post(V1, { criterion_id: '
|
|
527
|
+
await post(V1, { criterion_id: 'brand-new-slug', criterion_md: 'x', goal_id: 3 });
|
|
528
528
|
assert.ok(versionReads.length > 0, 'the route read the version');
|
|
529
529
|
for (const v of versionReads) {
|
|
530
530
|
assert.ok(v.deps && v.deps.client, 'the version was read outside the transaction');
|
|
@@ -566,6 +566,14 @@ await test('setGoalStatus writes on the transaction client (task 1003840)', asyn
|
|
|
566
566
|
assert.match(seen[1].sql, /UPDATE goal_membership_requests/);
|
|
567
567
|
});
|
|
568
568
|
|
|
569
|
+
await test('POST refuses a Cn-shaped criterion_id with a 400 before any SQL runs (task 1003122)', async () => {
|
|
570
|
+
actor = ARCHON;
|
|
571
|
+
const r = await post(V1, { criterion_id: 'C5', criterion_md: 'x', goal_id: 3 });
|
|
572
|
+
assert.equal(r.status, 400);
|
|
573
|
+
assert.equal(r.body.error, 'reserved_criterion_slug');
|
|
574
|
+
assert.equal(sqlLog.length, 0, 'refused before the transaction took any lock');
|
|
575
|
+
});
|
|
576
|
+
|
|
569
577
|
// The wrapper installed above is removed, so nothing after this file's own tests
|
|
570
578
|
// inherits a patched module export.
|
|
571
579
|
db.getGoal = realGetGoal;
|
|
@@ -16,6 +16,7 @@ import { createRequire } from 'node:module';
|
|
|
16
16
|
|
|
17
17
|
const require = createRequire(import.meta.url);
|
|
18
18
|
const db = require('../modules/lifecycle/db.js');
|
|
19
|
+
const { isReservedCriterionSlug } = require('../modules/lifecycle/criterion-ref.js');
|
|
19
20
|
|
|
20
21
|
let passed = 0;
|
|
21
22
|
let failed = 0;
|
|
@@ -51,6 +52,20 @@ test('a "Cn" embedded in a longer string does NOT match (anchored)', () => {
|
|
|
51
52
|
assert.equal(db.parseCriterionRef('see C8 above').kind, 'slug');
|
|
52
53
|
});
|
|
53
54
|
|
|
55
|
+
test('a Cn-shaped slug is reserved; real slugs are not (task 1003122)', () => {
|
|
56
|
+
for (const r of ['C5', 'c10', ' C1 ']) assert.equal(isReservedCriterionSlug(r), true, r);
|
|
57
|
+
for (const r of ['C', 'C8x', 'wa6-kernel', 'C-5', '5', '']) assert.equal(isReservedCriterionSlug(r), false, r);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("core_272 renames all ten of goal 1000045's Cn slugs to non-Cn names (task 1003122)", () => {
|
|
61
|
+
const sql = require('node:fs').readFileSync(new URL('../migrations/core_272_rename_cn_criterion_slugs.sql', import.meta.url), 'utf8');
|
|
62
|
+
const pairs = [...sql.matchAll(/\('(C\d+)',\s*'([^']+)'\)/g)].map((m) => [m[1], m[2]]);
|
|
63
|
+
assert.deepEqual(pairs.map((p) => p[0]), ['C1','C2','C3','C4','C5','C6','C7','C8','C9','C10']);
|
|
64
|
+
assert.equal(new Set(pairs.map((p) => p[1])).size, 10, 'new slugs are distinct');
|
|
65
|
+
for (const [, n] of pairs) assert.equal(isReservedCriterionSlug(n), false, n);
|
|
66
|
+
assert.match(sql, /goal_id = 1000045/);
|
|
67
|
+
});
|
|
68
|
+
|
|
54
69
|
// --- resolveCriterionIds: the multi-goal refusal (task 1003108) -------------
|
|
55
70
|
// resolveCriterionIds is DB-backed, but it takes an `executor` seam (the one
|
|
56
71
|
// countTasksWithoutCriterionLink uses), so a fake SQL-recording client covers the
|
|
@@ -125,6 +140,32 @@ async function main() {
|
|
|
125
140
|
assert.deepEqual(r, { ids: [], unresolved: [], ambiguous: [] });
|
|
126
141
|
});
|
|
127
142
|
|
|
143
|
+
await atest('a criterion whose slug is literally "C5" is read positionally, never by slug (task 1003122)', async () => {
|
|
144
|
+
// The incident shape: goal 1000045 slugs ten criteria C1..C10. The ref "C5"
|
|
145
|
+
// takes the positional branch on a single-goal version (the 5th row, NOT the
|
|
146
|
+
// row slugged C5) and is refused on a multi-goal one, so the slug is
|
|
147
|
+
// unreachable — which is why creation now refuses the shape.
|
|
148
|
+
const rows = [
|
|
149
|
+
{ id: 601, criterion_id: 'alpha', cnum: 1 },
|
|
150
|
+
{ id: 602, criterion_id: 'C1', cnum: 2 },
|
|
151
|
+
{ id: 603, criterion_id: 'C5', cnum: 3 },
|
|
152
|
+
];
|
|
153
|
+
const ex = (goals) => ({ async query(sql) { return /FROM goals/.test(sql) ? { rows: [{ n: goals }] } : { rows }; } });
|
|
154
|
+
const single = await db.resolveCriterionIds('V9', ['C1'], ex(1));
|
|
155
|
+
assert.deepEqual(single.ids, [601], 'C1 means position 1, not the row slugged C1 (602)');
|
|
156
|
+
const multi = await db.resolveCriterionIds('V9', ['C5'], ex(2));
|
|
157
|
+
assert.deepEqual(multi.ids, []);
|
|
158
|
+
assert.deepEqual(multi.ambiguous, ['C5']);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await atest('createCriterion refuses a Cn slug before touching the database (task 1003122)', async () => {
|
|
162
|
+
const ex = { query: async () => { throw new Error('must not query'); } };
|
|
163
|
+
await assert.rejects(
|
|
164
|
+
db.createCriterion({ versionId: 'V9', criterionId: 'C5', criterionMd: 'x', goalId: 1 }, { client: ex }),
|
|
165
|
+
(e) => e.code === 'RESERVED_CRITERION_SLUG'
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
128
169
|
console.log(`\ncriterion ref parse (task 438) + multi-goal refusal (task 1003108) tests: ${passed} passed, ${failed} failed`);
|
|
129
170
|
process.exit(failed > 0 ? 1 : 0);
|
|
130
171
|
}
|