@bongos/core 1.19.585 → 1.19.587
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 +54 -29
- package/.claude/skills/idea-triage/SKILL.md +24 -4
- package/docs/adr/0261-a-preselect-always-carries-a-reason-the-bundle-summary-is-the-floor.md +78 -0
- package/docs/adr/0262-a-bug-never-lands-in-the-inbox.md +96 -0
- package/docs/adr/README.md +2 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/discord/discord-bugs.js +23 -2
- package/modules/discord/discord-inbound.js +9 -0
- package/modules/ideas/homeless-home.js +51 -0
- package/modules/ideas/inbox.js +33 -18
- package/modules/ideas/routes/inbox.js +17 -0
- package/modules/ideas/routing.js +65 -2
- package/modules/lifecycle/db-goals.js +92 -0
- package/modules/lifecycle/db-versions.js +30 -0
- package/modules/lifecycle/db.js +4 -1
- package/modules/lifecycle/goal-advisory.js +17 -1
- package/modules/lifecycle/lifecycle.js +11 -0
- package/modules/public-landing/public/projects.html +21 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/filing_abuse_matrix.mjs +99 -0
- package/tests/idea_routing.mjs +226 -1
- package/tests/maintenance_goal.mjs +191 -0
- package/tests/projects_hub_module_picker.mjs +29 -11
- package/tests/task_goal_required.mjs +2 -1
- package/tests/wizard_preselect_why.mjs +137 -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.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.587",
|
|
6
|
+
"core_contract": "1.19.587",
|
|
7
|
+
"source_commit": "cd407ba54d0bdbe3638949084ba731c21179c811",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-08T00:16:55.401Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 455,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2060,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2539,
|
|
20
|
+
"tree_sha256": "20c7a1a223a182078ca9a6e5dc0b6ca8d715a6e8b0a47b08a9c4dcb8dac2c74a",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
{
|
|
183
183
|
"path": ".claude/skills/idea-triage/SKILL.md",
|
|
184
184
|
"mode": "0000644",
|
|
185
|
-
"sha256": "
|
|
185
|
+
"sha256": "348bb6e8d27610848fb5f31b672c2c762f56cfb17141f7abe1a8dee0b5030bdd"
|
|
186
186
|
},
|
|
187
187
|
{
|
|
188
188
|
"path": ".claude/skills/ideate/SKILL.md",
|
|
@@ -1819,10 +1819,20 @@
|
|
|
1819
1819
|
"mode": "0000644",
|
|
1820
1820
|
"sha256": "fa04f8d71fff8186ae975a949c045ca7481bd0f22f395cb8c7397bbe03ddd449"
|
|
1821
1821
|
},
|
|
1822
|
+
{
|
|
1823
|
+
"path": "docs/adr/0261-a-preselect-always-carries-a-reason-the-bundle-summary-is-the-floor.md",
|
|
1824
|
+
"mode": "0000644",
|
|
1825
|
+
"sha256": "04382d76f04c9317b565fb72951b36503e67acc95981a388d5c45efc80faaa26"
|
|
1826
|
+
},
|
|
1827
|
+
{
|
|
1828
|
+
"path": "docs/adr/0262-a-bug-never-lands-in-the-inbox.md",
|
|
1829
|
+
"mode": "0000644",
|
|
1830
|
+
"sha256": "cc30ad9de02409b6e3db26517bef2966133b3362903a984b65d94ea289fd0d88"
|
|
1831
|
+
},
|
|
1822
1832
|
{
|
|
1823
1833
|
"path": "docs/adr/README.md",
|
|
1824
1834
|
"mode": "0000644",
|
|
1825
|
-
"sha256": "
|
|
1835
|
+
"sha256": "9d58f1a823a763141df272149e58b2182aa2289dd94d69ba0b3999789d8fc743"
|
|
1826
1836
|
},
|
|
1827
1837
|
{
|
|
1828
1838
|
"path": "docs/api-reference.md",
|
|
@@ -2707,7 +2717,7 @@
|
|
|
2707
2717
|
{
|
|
2708
2718
|
"path": "docs/module-api-changelog.md",
|
|
2709
2719
|
"mode": "0000644",
|
|
2710
|
-
"sha256": "
|
|
2720
|
+
"sha256": "3e6075bb21834fc14b6201155cac3f4b51e262d86d6ccbed2505f593831f59f9"
|
|
2711
2721
|
},
|
|
2712
2722
|
{
|
|
2713
2723
|
"path": "docs/modules-contract.md",
|
|
@@ -4192,7 +4202,7 @@
|
|
|
4192
4202
|
{
|
|
4193
4203
|
"path": "modules/discord/discord-bugs.js",
|
|
4194
4204
|
"mode": "0000644",
|
|
4195
|
-
"sha256": "
|
|
4205
|
+
"sha256": "52b463955cd7e57f8d194c161a76e2ade83481f3ee0a16759019a5b1f873b374"
|
|
4196
4206
|
},
|
|
4197
4207
|
{
|
|
4198
4208
|
"path": "modules/discord/discord-channels.js",
|
|
@@ -4202,7 +4212,7 @@
|
|
|
4202
4212
|
{
|
|
4203
4213
|
"path": "modules/discord/discord-inbound.js",
|
|
4204
4214
|
"mode": "0000644",
|
|
4205
|
-
"sha256": "
|
|
4215
|
+
"sha256": "86099070386ff606ec14330375ac8c3145365307ae2ac354539731aebcc16563"
|
|
4206
4216
|
},
|
|
4207
4217
|
{
|
|
4208
4218
|
"path": "modules/discord/discord-message-buffer.js",
|
|
@@ -5359,10 +5369,15 @@
|
|
|
5359
5369
|
"mode": "0000644",
|
|
5360
5370
|
"sha256": "07e5666362c91f92d34388b4f6f4507cc5387866d317970478e6cae417c19975"
|
|
5361
5371
|
},
|
|
5372
|
+
{
|
|
5373
|
+
"path": "modules/ideas/homeless-home.js",
|
|
5374
|
+
"mode": "0000644",
|
|
5375
|
+
"sha256": "363598b5905c850967b407f20110a1d9e9d5b1090a958700cb6a4ed5cea70d80"
|
|
5376
|
+
},
|
|
5362
5377
|
{
|
|
5363
5378
|
"path": "modules/ideas/inbox.js",
|
|
5364
5379
|
"mode": "0000644",
|
|
5365
|
-
"sha256": "
|
|
5380
|
+
"sha256": "8c0792629df6d9587bce828a0f3743b8c86bdb31ba226d31211b9c6623574395"
|
|
5366
5381
|
},
|
|
5367
5382
|
{
|
|
5368
5383
|
"path": "modules/ideas/migrations/ideas_001_grades.sql",
|
|
@@ -5407,12 +5422,12 @@
|
|
|
5407
5422
|
{
|
|
5408
5423
|
"path": "modules/ideas/routes/inbox.js",
|
|
5409
5424
|
"mode": "0000644",
|
|
5410
|
-
"sha256": "
|
|
5425
|
+
"sha256": "8dec7682099d131ca050487dafe259bf4c9b511103c472cd0cbce54b4b9a898f"
|
|
5411
5426
|
},
|
|
5412
5427
|
{
|
|
5413
5428
|
"path": "modules/ideas/routing.js",
|
|
5414
5429
|
"mode": "0000644",
|
|
5415
|
-
"sha256": "
|
|
5430
|
+
"sha256": "ddedb76f43edbe3e7c918578fda94b97cb3822d97074af33393791fb1ec1cbef"
|
|
5416
5431
|
},
|
|
5417
5432
|
{
|
|
5418
5433
|
"path": "modules/ideas/templates.js",
|
|
@@ -5487,7 +5502,7 @@
|
|
|
5487
5502
|
{
|
|
5488
5503
|
"path": "modules/lifecycle/db-goals.js",
|
|
5489
5504
|
"mode": "0000644",
|
|
5490
|
-
"sha256": "
|
|
5505
|
+
"sha256": "f50a54a0e0f0cdeac5a2321b14757e52d9d9507417b1d1962aa09cdc6a222832"
|
|
5491
5506
|
},
|
|
5492
5507
|
{
|
|
5493
5508
|
"path": "modules/lifecycle/db-grade.js",
|
|
@@ -5522,12 +5537,12 @@
|
|
|
5522
5537
|
{
|
|
5523
5538
|
"path": "modules/lifecycle/db-versions.js",
|
|
5524
5539
|
"mode": "0000644",
|
|
5525
|
-
"sha256": "
|
|
5540
|
+
"sha256": "8214d4a7c2e97b12e8cd9ba866b6560ac6e67446af5dcf28a0055838e32897f5"
|
|
5526
5541
|
},
|
|
5527
5542
|
{
|
|
5528
5543
|
"path": "modules/lifecycle/db.js",
|
|
5529
5544
|
"mode": "0000644",
|
|
5530
|
-
"sha256": "
|
|
5545
|
+
"sha256": "6d69634ad12510f81727eeed0a8fdde4eac618fedc23e43e2c1aa5835aaf0388"
|
|
5531
5546
|
},
|
|
5532
5547
|
{
|
|
5533
5548
|
"path": "modules/lifecycle/dead-deps.js",
|
|
@@ -5552,7 +5567,7 @@
|
|
|
5552
5567
|
{
|
|
5553
5568
|
"path": "modules/lifecycle/goal-advisory.js",
|
|
5554
5569
|
"mode": "0000644",
|
|
5555
|
-
"sha256": "
|
|
5570
|
+
"sha256": "d17e49302bde2677927467c01acc40060f0abfcb7d8361354f9b092dd9859b55"
|
|
5556
5571
|
},
|
|
5557
5572
|
{
|
|
5558
5573
|
"path": "modules/lifecycle/goal-authz.js",
|
|
@@ -5607,7 +5622,7 @@
|
|
|
5607
5622
|
{
|
|
5608
5623
|
"path": "modules/lifecycle/lifecycle.js",
|
|
5609
5624
|
"mode": "0000644",
|
|
5610
|
-
"sha256": "
|
|
5625
|
+
"sha256": "db5e823ab382d44d4ae41266cec8ff76a98d6e636fd175cea0f1a04135eec7c9"
|
|
5611
5626
|
},
|
|
5612
5627
|
{
|
|
5613
5628
|
"path": "modules/lifecycle/merge-lock.js",
|
|
@@ -6832,7 +6847,7 @@
|
|
|
6832
6847
|
{
|
|
6833
6848
|
"path": "modules/public-landing/public/projects.html",
|
|
6834
6849
|
"mode": "0000644",
|
|
6835
|
-
"sha256": "
|
|
6850
|
+
"sha256": "eca7a7b580db8a794f5e0e17c8bfbb106529f14bf43c6e627d8f32e1dc9d6a53"
|
|
6836
6851
|
},
|
|
6837
6852
|
{
|
|
6838
6853
|
"path": "modules/public-landing/public/projects.probes.json",
|
|
@@ -7577,12 +7592,12 @@
|
|
|
7577
7592
|
{
|
|
7578
7593
|
"path": "package-lock.json",
|
|
7579
7594
|
"mode": "0000644",
|
|
7580
|
-
"sha256": "
|
|
7595
|
+
"sha256": "2b1a6ffdd207887a187f1574571de67e3f8ba851e80e433d463e932316b36e16"
|
|
7581
7596
|
},
|
|
7582
7597
|
{
|
|
7583
7598
|
"path": "package.json",
|
|
7584
7599
|
"mode": "0000644",
|
|
7585
|
-
"sha256": "
|
|
7600
|
+
"sha256": "0caa3d71c5b02c8a41cebc7e59a6af2d68b50eb419311c7d444b0daa30df8430"
|
|
7586
7601
|
},
|
|
7587
7602
|
{
|
|
7588
7603
|
"path": "public-docs/index.html",
|
|
@@ -9292,7 +9307,7 @@
|
|
|
9292
9307
|
{
|
|
9293
9308
|
"path": "src/module-api.js",
|
|
9294
9309
|
"mode": "0000644",
|
|
9295
|
-
"sha256": "
|
|
9310
|
+
"sha256": "6a4b0d97d9bc67728886e879359cf1f657dd81f865d141d8a1c6cd6e41877012"
|
|
9296
9311
|
},
|
|
9297
9312
|
{
|
|
9298
9313
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10282,7 +10297,7 @@
|
|
|
10282
10297
|
{
|
|
10283
10298
|
"path": "tests/filing_abuse_matrix.mjs",
|
|
10284
10299
|
"mode": "0000644",
|
|
10285
|
-
"sha256": "
|
|
10300
|
+
"sha256": "2a6de0e7411bb4796fe4f301af77c33f08a99307f2298ed83cd32c38cf059dc5"
|
|
10286
10301
|
},
|
|
10287
10302
|
{
|
|
10288
10303
|
"path": "tests/first_admin_bootstrap.mjs",
|
|
@@ -11007,7 +11022,7 @@
|
|
|
11007
11022
|
{
|
|
11008
11023
|
"path": "tests/idea_routing.mjs",
|
|
11009
11024
|
"mode": "0000644",
|
|
11010
|
-
"sha256": "
|
|
11025
|
+
"sha256": "d4d4e6ce60f1524e821e0c6c9f2844c7ba791b5502072dc00496224404afa990"
|
|
11011
11026
|
},
|
|
11012
11027
|
{
|
|
11013
11028
|
"path": "tests/idea_spark_hall.mjs",
|
|
@@ -11229,6 +11244,11 @@
|
|
|
11229
11244
|
"mode": "0000644",
|
|
11230
11245
|
"sha256": "9a1813d6036886ea07d1563e71a466e66c28554b75b22605464c795fd9fa2ebb"
|
|
11231
11246
|
},
|
|
11247
|
+
{
|
|
11248
|
+
"path": "tests/maintenance_goal.mjs",
|
|
11249
|
+
"mode": "0000644",
|
|
11250
|
+
"sha256": "645fe3e9093abcb1425a59602d6a92a6d90669775aa3503ae435a54c79eca5c1"
|
|
11251
|
+
},
|
|
11232
11252
|
{
|
|
11233
11253
|
"path": "tests/manage_manifest_shared_read.mjs",
|
|
11234
11254
|
"mode": "0000644",
|
|
@@ -11702,7 +11722,7 @@
|
|
|
11702
11722
|
{
|
|
11703
11723
|
"path": "tests/projects_hub_module_picker.mjs",
|
|
11704
11724
|
"mode": "0000644",
|
|
11705
|
-
"sha256": "
|
|
11725
|
+
"sha256": "bae4d97b96d9aa747e70ee3a62f40bb7ef16c3702f4c2be362b12fe814a60aa6"
|
|
11706
11726
|
},
|
|
11707
11727
|
{
|
|
11708
11728
|
"path": "tests/projects_hub_pre_uat.mjs",
|
|
@@ -12472,7 +12492,7 @@
|
|
|
12472
12492
|
{
|
|
12473
12493
|
"path": "tests/task_goal_required.mjs",
|
|
12474
12494
|
"mode": "0000644",
|
|
12475
|
-
"sha256": "
|
|
12495
|
+
"sha256": "2e311d02f87c562c2e8ed7cec24100efe3d1c750cb145e9e9e6bb1dac8043228"
|
|
12476
12496
|
},
|
|
12477
12497
|
{
|
|
12478
12498
|
"path": "tests/task_lib.mjs",
|
|
@@ -12654,6 +12674,11 @@
|
|
|
12654
12674
|
"mode": "0000644",
|
|
12655
12675
|
"sha256": "74ffbc7e4585e651d00ab879ece6f3d23089d7c9ae7acb15483d3752b47bbdc0"
|
|
12656
12676
|
},
|
|
12677
|
+
{
|
|
12678
|
+
"path": "tests/wizard_preselect_why.mjs",
|
|
12679
|
+
"mode": "0000644",
|
|
12680
|
+
"sha256": "2dc90311d8a4f31b543ed7ef711b4d3fc8b387a241bf0bbe797ab360107eac38"
|
|
12681
|
+
},
|
|
12657
12682
|
{
|
|
12658
12683
|
"path": "tests/workflow_exec_paths.mjs",
|
|
12659
12684
|
"mode": "0000644",
|
|
@@ -1,18 +1,38 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: idea-triage
|
|
3
|
-
description:
|
|
3
|
+
description: >-
|
|
4
|
+
Daily walk of the Bongos idea_inbox, which holds only HOMELESS work: a capture
|
|
5
|
+
naming a goal, and since ADR 0262 a bug from a trusted vector, become a task at
|
|
6
|
+
filing time and never reach it. Promote, discard or merge each open idea.
|
|
7
|
+
Triggers on "/idea-triage", "triage ideas", "review the inbox", "walk the idea
|
|
8
|
+
inbox", or a scheduled daily run. Metic+ only.
|
|
4
9
|
---
|
|
5
10
|
|
|
6
11
|
You are running the daily idea-triage session for Example. Open ideas in the Bongos `idea_inbox` table get walked one-at-a-time and verdicted, so the inbox doesn't accumulate stale entries.
|
|
7
12
|
|
|
8
|
-
## The inbox is HOMELESS-ONLY now (task
|
|
13
|
+
## The inbox is HOMELESS-ONLY now (task 1003065 / R01 + task 1003691, goal 1000071)
|
|
9
14
|
|
|
10
|
-
**What changed.** Since R01 (task 1003065) a
|
|
11
|
-
filing time — it becomes a task in
|
|
15
|
+
**What changed.** Since R01 (task 1003065) a capture that NAMES A GOAL routes at
|
|
16
|
+
filing time — it becomes a task in that goal, in one transaction, and never appears
|
|
12
17
|
here. The inbox is therefore no longer "everything anyone filed"; it holds only
|
|
13
18
|
**work with no home yet**. Expect it to be smaller, and read what IS in it as
|
|
14
19
|
work that genuinely needs a decision rather than a queue to clear.
|
|
15
20
|
|
|
21
|
+
**Bugs (task 1003691, [ADR 0262](../../../docs/adr/<redacted>.md)).**
|
|
22
|
+
A `kind='bug'` now routes even with NO goal named: the system files it into the
|
|
23
|
+
version's maintenance goal rather than making the reporter know the goal map. So a
|
|
24
|
+
bug should not appear in this queue — with **one deliberate exception**. Routing is
|
|
25
|
+
gated on the VECTOR, and Discord `#ideas` is not on the allowlist: it passes no
|
|
26
|
+
kind, so the classifier GUESSES one from keywords ("we should fix the copy" reads
|
|
27
|
+
as a bug), and a guess must not mint work (ADR 0234's owner-interview decision).
|
|
28
|
+
Bug reports from Discord have their own channel, `#bugs`, which does route.
|
|
29
|
+
|
|
30
|
+
> An earlier version of this section claimed bugs "never appear here" and credited
|
|
31
|
+
> task 1003071. That was false when written — 1003071 only touched the
|
|
32
|
+
> triage/promote path (commit `dab7e314`), while capture-time routing was 1003065,
|
|
33
|
+
> whose no-goal early return made the bug branch unreachable. If you are triaging a
|
|
34
|
+
> bug today, it arrived from `#ideas` or from a vector predating ADR 0262.
|
|
35
|
+
|
|
16
36
|
**Two consequences for how you run this skill:**
|
|
17
37
|
|
|
18
38
|
1. **Every promote names a goal.** The promote step now asks for one, defaulted
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# ADR 0261 — A preselect always carries a reason; the bundle's summary is the floor
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted · 2026-09-07 · task 1003684
|
|
4
|
+
**Supersedes nothing. Resolves a tension between [ADR 0237](<redacted>.md) and [ADR 0243](<redacted>.md).**
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Two rules were each right on their own and wrong together.
|
|
9
|
+
|
|
10
|
+
**ADR 0243** made `bundle.adjustments` a **delta**: a team-shape rule that fires without moving
|
|
11
|
+
anything claims nothing. That is correct — a sentence explaining a change that did not happen is
|
|
12
|
+
a claim the owner cannot check, and the module contract states it plainly ("a rule that changed
|
|
13
|
+
nothing claims nothing").
|
|
14
|
+
|
|
15
|
+
**ADR 0237** says a preselect the owner cannot see a reason for is one they have to audit, which
|
|
16
|
+
is worse than no preselect at all. The wizard's own paint site quotes this.
|
|
17
|
+
|
|
18
|
+
The two collide whenever a type's bundle **already contains** what a rule would add. The live
|
|
19
|
+
case: `game` is `['dev-box','discord']` and the `small-team` rule adds `dev-box`. The rule fires,
|
|
20
|
+
moves nothing, and correctly reports `adjustments: []`. Confirmed against live core 1.19.580:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
GET /provisioning/starter-bundles → bundles[type=game].byTeamShape['small-team']
|
|
24
|
+
{"bundle":["dev-box","discord"],"adjustments":[]}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The owner then reached step 4 of the create wizard and saw **two extras switched on and no reason
|
|
28
|
+
beside them** — `#modWhy` rendered as an empty, hidden div. Exactly the state ADR 0237 exists to
|
|
29
|
+
prevent, produced by ADR 0243 behaving correctly.
|
|
30
|
+
|
|
31
|
+
This was originally filed as a bug against the recommendation engine (a "dropped adjustment").
|
|
32
|
+
It was not: the engine is right. The gap is that the panel had only one voice — the rule's — and
|
|
33
|
+
fell silent when the rule had nothing to say.
|
|
34
|
+
|
|
35
|
+
## Decision
|
|
36
|
+
|
|
37
|
+
**The why-line has two voices, and the type's is the floor.**
|
|
38
|
+
|
|
39
|
+
1. When one or more rules **moved** something, their sentences are shown, unchanged. A real
|
|
40
|
+
adjustment still speaks for itself and is never displaced.
|
|
41
|
+
2. When no rule moved anything, the **type's own curated `summary`** explains the preselect.
|
|
42
|
+
3. When the owner has answered the picker for themselves (`state.modules !== null`), the panel
|
|
43
|
+
says nothing at all — unchanged. Handing back a reason for a toggle they just flipped reads
|
|
44
|
+
as the panel arguing with them.
|
|
45
|
+
4. A bundle carrying no summary still says nothing. The floor is a floor, not an invention.
|
|
46
|
+
|
|
47
|
+
The summary was already authored for this job (`STARTER_BUNDLES[type].summary` in
|
|
48
|
+
`modules/provisioning/starter-bundles.js`), already served on every row of
|
|
49
|
+
`GET /provisioning/starter-bundles`, and — until this change — **rendered nowhere in the
|
|
50
|
+
product**.
|
|
51
|
+
|
|
52
|
+
## Consequences
|
|
53
|
+
|
|
54
|
+
- No new copy, no new endpoint, no new field. The change is which existing sentence is rendered
|
|
55
|
+
when the delta is empty.
|
|
56
|
+
- `adjustments` keeps its meaning exactly. Nothing about ADR 0243 is weakened: the delta is still
|
|
57
|
+
a delta, and no rule is credited with a change it did not make. What changed is that the
|
|
58
|
+
panel's silence is no longer the *only* alternative to a rule sentence.
|
|
59
|
+
- The pin `tests/projects_hub_module_picker.mjs` had a fixture with **no `summary` field**, which
|
|
60
|
+
made four "no adjustment, no sentence" cases pass for the wrong reason. The fixture now carries
|
|
61
|
+
the real summaries and those cases assert the new contract — including, explicitly, that no
|
|
62
|
+
*rule* sentence is invented for a change that did not happen.
|
|
63
|
+
- `tests/wizard_preselect_why.mjs` pins the resolver directly across every combination.
|
|
64
|
+
|
|
65
|
+
## Alternatives rejected
|
|
66
|
+
|
|
67
|
+
**Make the rule claim a no-op change.** Report `a-team-shares-one-environment` as an adjustment
|
|
68
|
+
even when `dev-box` was already in the bundle. Rejected: it re-introduces exactly the unverifiable
|
|
69
|
+
claim ADR 0243 removed, and the owner would read "a small team gets one shared environment" beside
|
|
70
|
+
a module that was on before they answered.
|
|
71
|
+
|
|
72
|
+
**Widen the bundles so no rule is ever redundant.** Rejected: it contorts curated per-type presets
|
|
73
|
+
to serve an unrelated rendering concern, and the redundancy would return the moment a rule or a
|
|
74
|
+
bundle changed.
|
|
75
|
+
|
|
76
|
+
**Leave it silent.** Rejected by the owner on 2026-09-07 after the trade-off was put to them
|
|
77
|
+
directly. The preselect is the wizard's most consequential default, and shipping it unexplained
|
|
78
|
+
for the most common answer combination is the failure ADR 0237 names.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# ADR 0262 — A bug never lands in the inbox: the system names the home the reporter didn't
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted · 2026-09-07 · task 1003691
|
|
4
|
+
**Supersedes nothing. Amends nothing.** It makes the explicit decision [ADR 0235](<redacted>.md) §6 said would be needed, and it keeps [ADR 0234](<redacted>.md)'s `#ideas` interview decision and [ADR 0250](<redacted>.md) D4's invariant both intact.
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
A reported defect could still end up parked in `idea_inbox`, waiting for a human triage pass — the exact tax criterion C1 of goal 1000071 was opened to remove.
|
|
9
|
+
|
|
10
|
+
The cause is one line. `modules/ideas/routing.js` `decideLanding()` opened with:
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
if (goalId == null || goalId === '') return { route: false, reason: 'no_goal' };
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The `kind === 'bug'` hard-land branch sat **nine lines below** it and was therefore **unreachable without a goal**. `kind` only ever chose the routed task's *status* (`ready` for a trusted filer, `backlog` otherwise); it never chose whether the filing routed at all. So "bugs are urgent-true" was true only for a reporter who already knew which goal the defect belonged to — which is precisely the knowledge criterion C5 promises a filer never needs.
|
|
17
|
+
|
|
18
|
+
### The conflict this task was filed over does not exist
|
|
19
|
+
|
|
20
|
+
The task was opened on the premise that two accepted ADRs contradict each other, and that one had to lose:
|
|
21
|
+
|
|
22
|
+
- **ADR 0250 D4** — "Every task belongs to a real, open goal. `goal_id` becomes required at creation." Enforced: `modules/lifecycle/db-tasks.js` throws, `POST /tasks` 400s `goal_id_required`.
|
|
23
|
+
- **ADR 0235** — read as exempting `kind='bug'` from needing a goal at all, on the strength of the phrase *"a fix needs no home to be filed correctly."*
|
|
24
|
+
|
|
25
|
+
**That reading of 0235 is wrong, and checking it was the first thing this task did.** ADR 0235's headline decision is the opposite:
|
|
26
|
+
|
|
27
|
+
> **A fix category is `kind='bug'` on a task that carries a real `goal_id`** — no schema change, no new column, no new goal type.
|
|
28
|
+
|
|
29
|
+
The quoted phrase describes `adviseGoal()` in `modules/lifecycle/goal-advisory.js:64`, which returns `null` — stays silent — for a `kind='bug'` task on its `no_goal`, `catch_all` and `no_criterion_link` branches. It is an **advisory suppression**: it stops the system nagging a bug about its goal. It grants nothing, and it changes no schema.
|
|
30
|
+
|
|
31
|
+
So both ADRs already require a real goal, nothing loses, and no amendment is owed. What ADR 0235 *did* anticipate is this task — §6 Consequences:
|
|
32
|
+
|
|
33
|
+
> giving `kind='bug'` more weight than an advisory exemption "is a new, explicit decision needing its own ADR."
|
|
34
|
+
|
|
35
|
+
This is that ADR.
|
|
36
|
+
|
|
37
|
+
## Decision
|
|
38
|
+
|
|
39
|
+
**A `kind='bug'` filed from a trusted vector with no goal becomes a task in the version's maintenance goal, at the status the existing rank split already assigns.** Two halves, and both are load-bearing.
|
|
40
|
+
|
|
41
|
+
### D1 — The home is the per-version MAINTENANCE goal, find-or-created
|
|
42
|
+
|
|
43
|
+
The reporter does not name a home; the system does. `modules/ideas/homeless-home.js` resolves the current building version (`lifecycle.currentBuildingVersionId`), then find-or-creates `"<version> — maintenance"` (`lifecycle.ensureMaintenanceGoal`).
|
|
44
|
+
|
|
45
|
+
**Not the `"<version> — general"` catch-all**, which is the obvious-looking choice and the wrong one. BV1.R11 (task 1003598) began *deleting* that fallback after 53 open tasks accumulated unread in two of them, and `db-tasks.js` warns against adding a third caller to the `allowCatchAll` opt-out it left behind. The maintenance goal is the home task 1003605 (BV1.R18) already designs for exactly this — *"where a defect against already-shipped work lands"* — so routing points at the destination the roadmap was already heading for rather than repopulating the bucket being drained. **This vector adds no `allowCatchAll` caller**, pinned by `tests/task_goal_required.mjs`.
|
|
46
|
+
|
|
47
|
+
ADR 0250 D4's invariant therefore holds exactly: the task carries a real, open `goal_id`. What changes is only *who names it*.
|
|
48
|
+
|
|
49
|
+
Three mechanics that are decisions, not implementation detail:
|
|
50
|
+
|
|
51
|
+
- **The version tiebreak is not new.** More than one version may sit at `building` — ADR 0250 D5 closes the *planning* slot only, and refusing a second building version is still unshipped (task 1003606, BV1.R19). The rule reused is `pickDefaultVersion` from `scripts/gds/version-select.js`, expressed in SQL: building only, `started_at DESC`, nulls last, `id` breaking a remaining tie.
|
|
52
|
+
- **Find-or-create takes a `pg_advisory_xact_lock`.** `goals` has no unique constraint on `(version_id, title)` — migration 160 used a `NOT EXISTS` guard, safe for a one-shot backfill and unsafe for a concurrent path, where two bugs reported in the same second would both insert and leave the title lookup permanently ambiguous.
|
|
53
|
+
- **It runs inside the caller's transaction.** A goal created on its own connection would outlive a rolled-back route as an empty goal nobody asked for.
|
|
54
|
+
|
|
55
|
+
**It fails closed.** No building version (`NO_BUILDING_VERSION`), or a version whose maintenance goal cannot be made (`NO_MAINTENANCE_GOAL`), refuses the filing and rolls back. Inventing a version is idea 1000771's exact failure — every Discord report entombed in a shipped version's catch-all — and this path exists to end that, not to repeat it.
|
|
56
|
+
|
|
57
|
+
### D2 — The gate is the VECTOR, not the kind alone
|
|
58
|
+
|
|
59
|
+
Routing keys on an allowlist, `routing.HOMELESS_ROUTING_VECTORS = {'api', 'discord-bugs'}`, and **absent or unrecognised is untrusted**.
|
|
60
|
+
|
|
61
|
+
This is the half that keeps ADR 0234 whole. Its owner-interview decision — *"Discord `#ideas` files with HINTS only, never routes"* — is not an incidental detail: `#ideas` passes **no kind at all**, so `inbox.classify()` guesses one from keywords, and its bug pattern fires on `fix|broken|bug|regression|crash|error` anywhere in the text. *"We should fix the copy on the landing page"* classifies as a bug. Gating on kind alone would let a keyword guess mint work and would have killed an explicit owner decision as a side effect.
|
|
62
|
+
|
|
63
|
+
The two trusted members each carry a human or a channel that means *this is broken*:
|
|
64
|
+
|
|
65
|
+
| Vector | Why it is trusted |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `api` | `POST /inbox`, authenticated builder, explicit `kind`. `scripts/gds/capture.js` posts to this route, so the CLI inherits it and needs no member of its own. |
|
|
68
|
+
| `discord-bugs` | The **channel is the classification** (`discord-bugs.js` forces `kind='bug'`), and it already carries an unlinked-author guard, a per-builder hourly rate cap, and a body screen. |
|
|
69
|
+
|
|
70
|
+
`discord-ideas` is named explicitly at its call site so its exclusion is a written fact rather than an absence someone has to notice. The vector is **server-set at every call site and never read from a request body**, which is what makes forging it a non-move.
|
|
71
|
+
|
|
72
|
+
#### A vector is claimed by a call site, so the call site must still check identity
|
|
73
|
+
|
|
74
|
+
`#bugs` sets `vector: builder ? 'discord-bugs' : null` — **conditional, not the bare string.** The first cut of this ADR set it unconditionally and that was a trust-boundary regression caught as a grader blocker, worth recording because the shape will recur.
|
|
75
|
+
|
|
76
|
+
The channel earns its place on the allowlist through two guards that are both **identity-bound**: the rank-aware landing needs a rank, and the rolling rate cap is counted per builder id. An unlinked Discord author has neither — `filerRank` is null and the goal is only sought `if (builder)` — so an unconditional vector meant they took the homeless-bug branch and minted a real `backlog` task, with the per-builder rate limiter not running at all for them. That is unattributed task creation at no cost, and it contradicts the promise at the top of `discord-bugs.js` that ADR 0047 bought: *"an unlinked author cannot inject claimable work… it no longer mints anything."*
|
|
77
|
+
|
|
78
|
+
The general rule: **being on the allowlist is a property of the DOOR, not of everyone who walks through it.** A vector attests "filings from here carry a human who means *this is broken*"; where a door admits both identified and anonymous traffic, it may only claim the vector for the identified half. With no vector the filing falls back to the inbox exactly as before — the report is kept, its images re-hosted, a human still sees it in triage. Pinned as F16, mutation-verified against the unconditional form.
|
|
79
|
+
|
|
80
|
+
Everything else the matrix already decided is unchanged and still applies to this path: the Metic+ rank split (`ready` vs `backlog`), the Full Idea refusal, and the judgment kinds. **Only bugs get a free home** — a homeless feature or cleanup is exactly the work the inbox is *for*, and fabricating homes for it would empty the inbox by lying.
|
|
81
|
+
|
|
82
|
+
## Consequences
|
|
83
|
+
|
|
84
|
+
- **The inbox's stated identity becomes true.** `.claude/skills/idea-triage/SKILL.md` claimed bugs "never appear here" and credited task 1003071 — which only touched the triage/promote path (commit `dab7e314`); capture-time routing was task 1003065. The claim was false when written and is corrected alongside this ADR; it is now true for the `api` and `discord-bugs` vectors.
|
|
85
|
+
- **A bug reported in Discord `#ideas` still lands in the inbox.** That is deliberate, and it is not a gap: bug reports from Discord have their own channel, `#bugs`, which routes.
|
|
86
|
+
- **`#bugs`' bad-guess fallback got better.** It re-files with `goalId: null` when a suggested goal refuses the landing; that path now reaches the maintenance goal instead of the inbox, so dropping a wrong guess costs a triage pass rather than the whole report.
|
|
87
|
+
- **Task 1003605 (BV1.R18) is partly delivered and still owed.** This ships the maintenance goal's title, find-or-create and routing. R18 still owns auto-creating it with every version, the close-exempt flag R16 reads, and carrying open bugs forward to the successor version on close.
|
|
88
|
+
- **A new file on the create path** — `modules/ideas/homeless-home.js`, added to the `tests/task_goal_required.mjs` roster so the anti-drift pin covers it.
|
|
89
|
+
|
|
90
|
+
## Alternatives rejected
|
|
91
|
+
|
|
92
|
+
- **Let a goal-less bug create a goal-less task** (an `allowCatchAll`-style escape hatch for bugs). Breaks ADR 0250 D4 one day after it was accepted, and every consumer assuming `goal_id` is non-null would need auditing. The invariant is worth more than the shortcut.
|
|
93
|
+
- **Reject a bug with no goal, listing open goals to pick from.** One rule, no exceptions — but a rejected report does not land *at all*, and Discord's bot fires and forgets, so every inbound bug from a channel that cannot retry would be silently lost. It also directly contradicts the thing being asked for.
|
|
94
|
+
- **Route into the `"<version> — general"` catch-all.** Smallest diff, reuses shipped machinery, and repopulates the exact buckets BV1.R11 was filed to drain. R18 would then have to undo it.
|
|
95
|
+
- **Build R18 whole first, then route.** The cleanest end state, and substantially more scope than the defect being fixed; the close-exempt flag and version-close carry-forward are independent of whether a reported bug finds a home today.
|
|
96
|
+
- **Gate on `kind` alone.** Cheapest fix, and it silently overturns ADR 0234's owner-interview decision by letting a keyword classifier mint tasks from an ideation channel.
|
package/docs/adr/README.md
CHANGED
|
@@ -352,3 +352,5 @@ This keeps the decision history honest and traceable.
|
|
|
352
352
|
| 0258 | [**The public CLI is a generated client-only package, and its file list is proven by running it** ([task 1003679](https://cloudbongos.com/builders#/task/1003679) · goal 1000054 — *A newcomer can build without the UI*). The core ships private as `@bongos/core` ([ADR 0108](<redacted>.md)), so a newcomer with no credential can install NOTHING and the web hall is the only way in — the owner's words: "how am I supposed to easily take on tasks as a new builder? I should be able to do everything without the UI." Open task 1002025 proposed publishing the core, which ships the SERVER and bypasses [ADR 0099](<redacted>.md)'s redaction pipeline (dormant until ~2026-11). **Decision: generate a separate public `@cloudbongos/cli` from the core and do not publish the core.** Four load-bearing parts. (1) SCOPED NAME, because npm shares one namespace between org names and unscoped packages — the bare `cloudbongos` is unpublishable *precisely because the owner owns that org*, which npm reports as "invalid" and reads like the name is taken; and a package name cannot be created on the npm website at all (it exists on first publish, which is why "Add Existing Package" answered `Forbidden`). (2) THE FILE LIST IS DECLARED AND PROVEN BY BEHAVIOUR, never computed — a static require-closure CANNOT answer this, because `src/module-api.js` is the doorway a module may only import ([ADR 0083](<redacted>.md)) and it *names* every kernel capability, so a static walk sees 33 `src/` files from `start.js` alone where runtime resolves **one** (`api-prefix.js`, which imports nothing) — the same trap as task 1003677's unsatisfiable done-when. So the test packs the tarball, installs it into an empty dir with NO repo, and runs every verb: it may fail for want of a session, never for a missing file. That caught two real holes on its first strengthened run — `clients/bongos-client/index.mjs`, reached by a dynamic `import()` no `require()` walk can see, and a hand-listed `files` array that dropped `clients/` and produced a tarball which installed cleanly then died on first use; both are now build-time refusals, and the `files` array is derived from the manifest. (3) `claim`/`ship`/`dev`/`serve`/`module`/`upgrade`/`onboard`/`doctor`/`exec`/`package-core` are ABSENT and the CLI says WHY plus the next step (`bongos shell` → a cloud box with the full CLI) — a bare `unknown command` teaches nothing, which is the exact failure being fixed. The supported journey is `login` → `start` → `shell`. (4) A REDACTION GATE fails the build closed on non-loopback IPv4 (RFC 5737 doc ranges exempt), token/key shapes, and every domain + owner login readable from the instance's own `config/branding.json` — needles come from host config so the core carries no instance identity ([ADR 0062 §7](<redacted>.md)); `cloudbongos.com` is allowlisted as public by design. No `repository` field while the core repo is private (it would 404 for every user and publish the owner's login for nothing). 31 files, one dependency (`undici`), version independent of the core's ([ADR 0161](<redacted>.md)). `@bongos/client` is VENDORED, not depended on — a public package depending on a private one is uninstallable. **The owner runs `npm publish`; a builder must not.** Task 1002025 is superseded. Rejected: publishing the core; deriving from a static closure (structurally impossible past the doorway); waiting for the mirror (dormant, and this is a build product not a source release); an unscoped name; depending on `@bongos/client`; a degraded `claim`; shipping all of `src/` to make lazy getters safe.](<redacted>.md) | cli / distribution / public surface |
|
|
353
353
|
| 0259 | [**A project’s departure from the public list is public, and the copy says so** ([task 1003672](https://cloudbongos.com/builders#/task/1003672) · goal 1000046 — *Project creation*). Decides the leak [ADR 0255](<redacted>.md) §6.2 deliberately filed rather than implied fixed. The public projects feed publishes a named list plus a count of anonymous rows, so when a project flips to stealth its named row LEAVES the identified block while the count RISES BY ONE — *“Mercury is gone, and there is one more black hole”* names it, and the moment, from a single diff of two snapshots. **Everything [ADR 0182](<redacted>.md) D2a does is defeated by it**, because the identity was never carried ON the anonymous row (`STEALTH_ANONYMOUS_KEEPS` is an allow-list precisely so a column added next year is anonymous by default) — it is carried by the ABSENCE of the named row that used to sit beside it. **Larger than the channel 0255 closed**, which needed repeated sampling and correlation; this one is exact and needs two reads. No `ORDER BY` change touches it: it is inherent in publishing a named list at all, so every fix is a product decision with a visible cost and the owner’s call, not a builder’s. **Decision: accept it, and make the copy say so.** Two reasons it is the right end of the trade and not merely the cheapest — (1) it is ALREADY the honest reading of the existing promise, since D2a made existence and count public *by design*, and a count that is public and truthful is a count whose CHANGES are public; (2) both alternatives damage the control they defend. A **delay window** means the owner clicks the privacy setting and is not private yet *without knowing it*, converting an information leak into a false belief — strictly worse, and the wrong direction for a privacy control. **Decoys** spend the count’s truthfulness (it is truthful today) on a guarantee that is still only probabilistic, and a decoy scheme needs some way to tell padding from real rows — the exact stable per-project key D2a forbids. The promise stealth actually keeps is about the project’s CONTENTS, not the TRANSITION: an anonymous row stays anonymous (no name, tagline, art, origin, or key surviving to the next snapshot); what is not private is stepping off a published list readers may keep copies of. So the copy — which read *“its name, and everything else about it, stay yours”*, every clause true and the paragraph as a whole overselling, because a reader takes it to cover the switch — now names the boundary in BOTH places an owner meets the setting (the choice’s blurb and the what-changes fold), in the plain register 0182’s interview fixed for this card. **A privacy control that oversells is worse than one that admits a limit**, the same standard 0255 §6 set for itself. **No behaviour changes** in `projects-feed.js` — the feed, the redaction allow-list and the keyed rotating permutation are decided FINISHED, not insufficient. `tests/projects_hub.mjs` pins the sentence in both surfaces (the copy IS the fix, so a future edit tightening the blurb must not quietly drop it) and re-applies the no-space-metaphor ban to it. Carries 0255 §6.3’s smaller residual so both live in one place: within one 24h epoch the permutation is stable, so same-day snapshots tell black holes apart (not name them) — accepted on the same reasoning, since the fix is a per-request permutation, which breaks pagination exactly as `ORDER BY random()` was rejected for. Guards recorded for any future attempt: no stable per-project key on an anonymous row, and no anonymous ordering an observer can predict or correlate. Rejected: the delay; decoys; dropping the named list (protects a privacy nobody asked for at the cost of the feature); and saying nothing, which is the failure mode this closes.](<redacted>.md) | platform-identity / privacy boundary / copy honesty |
|
|
354
354
|
| 0260 | [**An application IS the consent, and the hub’s own echo is the gate** ([task 1002972](https://cloudbongos.com/builders#/task/1002972) · goal 1000045, criterion C4). Privacy spec **D7** asks that a reviewer see what the applicant’s OWN profile rules would already show, evaluated LIVE at review time. The rule is ONE port composing the two EXISTING views — `getPublicProfileExtras` for a public account, `recruiter-sliver`’s own `sliverShapeFor` for a private one — because a third five-key lookalike **is** the new disclosure class D7 forbids. **The one deliberate difference from the D3 sliver:** `getRecruiterSliver` floors on recruiting REACH, and reusing it verbatim would be wrong in the direction that looks safe — `recruiter_discoverable` defaults to FALSE for a private account, so a private builder who deliberately applied would show their reviewer NOTHING and C4 would be satisfied by an empty box. D7 settles it: *“applying is an explicit act”*. The swap relaxes REACH only; `accountActiveSql` (active **and** terms accepted) and `hide_stats` still floor it, and the proof asserts both directions on the same account so the tempting refactor cannot pass quietly. **The federated route’s gate is the whole security story** ([ADR 0205](<redacted>.md) / security report 1000027): client credentials name a PROJECT and never a person, so `POST /sso/applicant-profile` authenticated alone is a bulk disclosure oracle over every account on the platform, private ones included. `applicationBacksProfileRead` demands a HUB-WRITTEN echo for exactly (this account, this client) inside the same 30-day window — a row only the hub’s own join relay creates — refusing **404, never 403**, and BEFORE any account is read (asserted on the statement log, because a gate that refuses after reading has already done the disclosure work). LIVE means nothing is stored at either end; the queue keys on the VOUCH and never on `github_login` (the one PUBLIC unauthenticated write makes an unvouched login an impersonation surface); and the hub client secret stays in core behind a narrow doorway port. **The path is complete but DORMANT until R09 ([task 1002285](https://cloudbongos.com/builders#/task/1002285)) ships the vouch writer** — nothing writes `applicant_github_id` today, and that dependency was missing from 1002972’s graph. Rejected: routing the port through `getRecruiterSliver`, a snapshot at apply time, resolving by `github_login`, and putting `loadIdpConfig` on the doorway.](<redacted>.md) | platform identity / privacy |
|
|
355
|
+
| 0261 | [**A preselect always carries a reason; the bundle’s summary is the floor** ([task 1003684](https://cloudbongos.com/builders#/task/1003684) · goal 1000046 — *Project creation*). Resolves a collision between two rules that were each right alone. [ADR 0243](<redacted>.md) made `adjustments` a **delta** — a team-shape rule that fires without moving anything claims nothing, because a sentence explaining a change that did not happen is a claim the owner cannot check. [ADR 0237](<redacted>.md) says a preselect the owner cannot see a reason for is one they must audit, which is worse than no preselect at all. The two collide whenever a type’s bundle **already contains** what a rule would add: `game` is `[dev-box, discord]` and the `small-team` rule adds `dev-box`, so the rule fires, moves nothing, and correctly reports `adjustments: []`. Confirmed on live core 1.19.580 — `byTeamShape[small-team]` = `{"bundle":["dev-box","discord"],"adjustments":[]}`. The owner then reached step 4 and saw **two extras switched on with no reason beside them**, `#modWhy` an empty hidden div: exactly what 0237 exists to prevent, produced by 0243 behaving correctly. Filed as a dropped adjustment; it was not — the engine is right, and the panel simply had ONE voice and fell silent when the rule had nothing to say. **Decision: the why-line has two voices and the type’s is the floor.** A real adjustment still speaks for itself and is never displaced; when none moved, the type’s own curated `summary` explains the preselect; when the owner has answered the picker themselves the panel stays silent (unchanged — a reason handed back for a toggle they just flipped reads as the panel arguing with them); a bundle with no summary still says nothing, because a floor is not an invention. **No new copy, endpoint or field** — the summary was authored for this job in `starter-bundles.js`, served on every row of `GET /provisioning/starter-bundles`, and rendered NOWHERE in the product until now. 0243 is not weakened: the delta stays a delta and no rule is credited with a change it did not make. The pin `tests/projects_hub_module_picker.mjs` carried a fixture with **no `summary` field**, which made four “no adjustment, no sentence” cases pass for the wrong reason; the fixture now carries the real summaries and those cases assert the new contract, including explicitly that no RULE sentence is invented for a change that did not happen. Rejected: making the rule claim a no-op change (re-introduces the unverifiable claim 0243 removed); widening the bundles so no rule is ever redundant (contorts curated presets for a rendering concern, and the redundancy returns on the next edit); and leaving it silent, declined by the owner once the trade-off was put to them directly.](<redacted>.md) | provisioning / starter bundles / owner-facing copy |
|
|
356
|
+
| 0262 | [**A bug never lands in the inbox: the system names the home the reporter didn't** ([task 1003691](https://cloudbongos.com/builders#/task/1003691) · goal 1000086 — *Strict versioning*). `routing.js` returned early on a goal-less filing, so its own `kind='bug'` hard-land branch was UNREACHABLE without a goal — `kind` only ever chose a routed task's STATUS, never whether it routed — and a reported defect sat in `idea_inbox` awaiting the triage pass criterion C1 exists to remove. The premise that [ADR 0250](<redacted>.md) D4 and [ADR 0235](<redacted>.md) contradict each other is FALSE: 0235's exemption is an advisory silence in `goal-advisory.adviseGoal`, and its actual decision already requires a real `goal_id`. So nothing is amended; this is the explicit decision 0235 §6 said would be needed. **D1** a homeless bug gets the version's MAINTENANCE goal (R18's design), find-or-created under an advisory lock inside the route's own transaction — not the catch-all R11 is deleting, and adding no `allowCatchAll` caller. **D2** the gate is the VECTOR (`api`, `discord-bugs`), not the kind: `#ideas` passes no kind, so the classifier guesses one from keywords, and gating on kind alone would silently overturn [ADR 0234](<redacted>.md)'s owner-interview decision. Rejected: goal-less bug tasks (breaks D4 a day after it landed); rejecting the filing (Discord cannot retry, so the report is lost); the catch-all (repopulates the bucket being drained); building R18 whole first.](<redacted>.md) | work intake / idea routing / the version-goal invariant |
|
|
@@ -1619,5 +1619,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1619
1619
|
landed since 1.19.583 with no explicit bump. run 34164936809. (task 1002620)
|
|
1620
1620
|
1.19.585 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1621
1621
|
landed since 1.19.584 with no explicit bump. run 34165392436. (task 1002620)
|
|
1622
|
+
1.19.586 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1623
|
+
landed since 1.19.585 with no explicit bump. run 34167072966. (task 1002620)
|
|
1624
|
+
1.19.587 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1625
|
+
landed since 1.19.586 with no explicit bump. run 34172785860. (task 1002620)
|
|
1622
1626
|
---------------------------------------------------------------------------
|
|
1623
1627
|
```
|
|
@@ -248,6 +248,22 @@ async function recordInboundBug({ discordUserId, content, imageUrls = [], messag
|
|
|
248
248
|
filerRank,
|
|
249
249
|
goalId: goal ? goal.id : null,
|
|
250
250
|
suggestedPriority: 3,
|
|
251
|
+
// task 1003691 (ADR 0262): the trusted-vector claim, and it is claimed ONLY
|
|
252
|
+
// FOR A LINKED BUILDER — `builder &&`, not the bare string.
|
|
253
|
+
//
|
|
254
|
+
// This channel earns the vector through guard 2 and guard 3 above, and both
|
|
255
|
+
// are identity-bound: the rank-aware landing needs a rank, and the rolling
|
|
256
|
+
// rate cap is counted per builder id. An UNLINKED author has neither. Handing
|
|
257
|
+
// them the vector unconditionally would let anyone who can post here mint
|
|
258
|
+
// unattributed backlog tasks at no cost — breaking guard 1's promise at the
|
|
259
|
+
// top of this file ("an unlinked author cannot inject claimable work... it no
|
|
260
|
+
// longer mints anything"), which is the security property ADR 0047 bought.
|
|
261
|
+
//
|
|
262
|
+
// With no vector the filing falls back to the inbox exactly as it did before
|
|
263
|
+
// ADR 0262: the report is still kept, still re-hosts its images, and still
|
|
264
|
+
// reaches a human through triage. Nothing is lost except the ability to write
|
|
265
|
+
// to the task table without an identity. Pinned as F16.
|
|
266
|
+
vector: builder ? 'discord-bugs' : null,
|
|
251
267
|
};
|
|
252
268
|
|
|
253
269
|
// THE HOME IS A SUGGESTION, NOT A CONDITION OF BEING HEARD. routeIdeaToGoal is
|
|
@@ -255,8 +271,13 @@ async function recordInboundBug({ discordUserId, content, imageUrls = [], messag
|
|
|
255
271
|
// scope-wall verdict, a goal archived mid-flight — rolls the idea row back too
|
|
256
272
|
// and the report is gone. That is the exact failure this task exists to end, and
|
|
257
273
|
// it would be caused by a goal WE guessed at, not by anything the reporter did.
|
|
258
|
-
// So a refusal drops the guess and files the report
|
|
259
|
-
//
|
|
274
|
+
// So a refusal drops the guess and re-files the report with no goal at all.
|
|
275
|
+
//
|
|
276
|
+
// Since task 1003691 that no longer means the inbox: a homeless bug from a
|
|
277
|
+
// trusted vector routes to the version's MAINTENANCE goal, so dropping a bad
|
|
278
|
+
// guess costs the reporter a triage pass rather than the whole report. A
|
|
279
|
+
// sub-Metic refused from a protected-scope goal therefore still gets a real
|
|
280
|
+
// task — at 'backlog', in an unscoped goal, with the protected goal untouched.
|
|
260
281
|
let idea;
|
|
261
282
|
try {
|
|
262
283
|
idea = await deps.captureIdea(capture);
|
|
@@ -189,6 +189,15 @@ async function recordInboundIdea({ discordUserId, content, imageUrls = [], messa
|
|
|
189
189
|
// tests/filing_abuse_matrix.mjs, which greps this exact call for it).
|
|
190
190
|
suggestedGoalId: hint ? hint.id : null,
|
|
191
191
|
suggestedVersion,
|
|
192
|
+
// task 1003691 (ADR 0262): named EXPLICITLY so this vector's exclusion is a
|
|
193
|
+
// written fact rather than an absence someone has to notice. '#ideas' is not
|
|
194
|
+
// on routing.HOMELESS_ROUTING_VECTORS, so a filing here NEVER becomes a task
|
|
195
|
+
// — which is ADR 0234's owner-interview decision ("#ideas files with hints
|
|
196
|
+
// only, never routes"), and the reason it holds even now that a homeless bug
|
|
197
|
+
// routes elsewhere: the kind here is a KEYWORD GUESS (inbox.classify fires
|
|
198
|
+
// 'bug' on fix/broken/crash), and a guess must not mint work. Bug reports
|
|
199
|
+
// from Discord have their own channel, #bugs, which does route.
|
|
200
|
+
vector: 'discord-ideas',
|
|
192
201
|
});
|
|
193
202
|
|
|
194
203
|
await auditDiscordBot(deps, {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/ideas/homeless-home.js — WHERE A REPORTED BUG GOES WHEN NOBODY NAMED A
|
|
4
|
+
// GOAL (task 1003691, [ADR 0262](../../docs/adr/0262-a-bug-never-lands-in-the-inbox.md)).
|
|
5
|
+
//
|
|
6
|
+
// THE PROBLEM THIS ANSWERS. routing.decideLanding returned early on a goal-less
|
|
7
|
+
// filing, which made its own kind==='bug' branch unreachable without a goal: a
|
|
8
|
+
// reported defect parked in idea_inbox waiting for the triage pass criterion C1
|
|
9
|
+
// exists to remove. Letting bugs route required answering "route into WHAT", and
|
|
10
|
+
// that answer is here.
|
|
11
|
+
//
|
|
12
|
+
// THE ANSWER IS THE VERSION'S MAINTENANCE GOAL, and deliberately NOT the
|
|
13
|
+
// "<version> — general" catch-all. The catch-all is what BV1.R11 (task 1003598)
|
|
14
|
+
// began deleting after 53 open tasks accumulated in two of them unread, and
|
|
15
|
+
// db-tasks.js warns against a third caller of its opt-out hatch. The maintenance
|
|
16
|
+
// goal is the home task 1003605 (BV1.R18) designs for exactly this — "where a
|
|
17
|
+
// defect against already-shipped work lands" — so a homeless bug gets a REAL,
|
|
18
|
+
// open goal and ADR 0250 D4 ("every task belongs to a goal") holds intact.
|
|
19
|
+
//
|
|
20
|
+
// ITS OWN FILE because inbox.js sits at the fitness line cap with no headroom, and
|
|
21
|
+
// because the concept deserves a name: this is one question ("what home does a
|
|
22
|
+
// homeless bug get") with one answer, and a reader looking for it should find a
|
|
23
|
+
// file rather than a paragraph inside a transaction.
|
|
24
|
+
|
|
25
|
+
// resolveHomelessHome — the goal id a homeless bug lands in, or a thrown refusal.
|
|
26
|
+
//
|
|
27
|
+
// TWO PROPERTIES, both load-bearing:
|
|
28
|
+
//
|
|
29
|
+
// * IT RUNS ON THE CALLER'S TRANSACTION CLIENT. Both port calls take it, so the
|
|
30
|
+
// find-or-create commits with the idea + task + lineage rows or not at all. A
|
|
31
|
+
// goal created on its own connection would outlive a rolled-back route as an
|
|
32
|
+
// empty goal nobody asked for.
|
|
33
|
+
// * IT FAILS CLOSED. No building version, or a version whose maintenance goal
|
|
34
|
+
// cannot be made, means there is nowhere honest to file — so it refuses rather
|
|
35
|
+
// than inventing a version. Filing a defect into a version not open to work is
|
|
36
|
+
// idea 1000771's exact failure (every Discord report entombed in a shipped
|
|
37
|
+
// version's catch-all), and this path exists to end that, not to repeat it.
|
|
38
|
+
//
|
|
39
|
+
// It resolves the VERSION first, which inverts the usual order: every other routed
|
|
40
|
+
// filing reads the version off the goal it was handed. There is no goal here yet,
|
|
41
|
+
// so the version has to come first — and once this returns, everything downstream
|
|
42
|
+
// runs unchanged against a real goal id.
|
|
43
|
+
async function resolveHomelessHome(lifecycle, client) {
|
|
44
|
+
const versionId = await lifecycle.currentBuildingVersionId({ client });
|
|
45
|
+
if (!versionId) throw { code: 'NO_BUILDING_VERSION' };
|
|
46
|
+
const home = await lifecycle.ensureMaintenanceGoal(versionId, { client });
|
|
47
|
+
if (!home) throw { code: 'NO_MAINTENANCE_GOAL', version_id: versionId };
|
|
48
|
+
return home.id;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { resolveHomelessHome };
|