@bongos/core 1.19.289 → 1.19.290
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 +20 -15
- package/.claude/skills/idea-triage/SKILL.md +32 -2
- package/docs/copy-inventory.md +15 -13
- package/docs/copy-registry.json +32 -14
- package/modules/hall-ui/public/task-detail.js +75 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/triage.js +98 -7
- package/src/module-api.js +3 -1
- package/tests/promote_goal_id.mjs +316 -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.290",
|
|
6
|
+
"core_contract": "1.19.290",
|
|
7
|
+
"source_commit": "a983fae1fa7615fda22b12d59ff393255fd81a19",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-22T04:46:05.299Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 320,
|
|
13
13
|
"agent_docs_stubbed": 22,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1389,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1731,
|
|
20
|
+
"tree_sha256": "f3100f6df33ff1d5f9e4da0d536a0cc792b0c7ad9e3c945c87324554b34105db",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
{
|
|
173
173
|
"path": ".claude/skills/idea-triage/SKILL.md",
|
|
174
174
|
"mode": "0000644",
|
|
175
|
-
"sha256": "
|
|
175
|
+
"sha256": "c74c93659c4576b32d829ebb2774feb8771125b31b677e070841f4cc25ed0232"
|
|
176
176
|
},
|
|
177
177
|
{
|
|
178
178
|
"path": ".claude/skills/ideate/SKILL.md",
|
|
@@ -1437,12 +1437,12 @@
|
|
|
1437
1437
|
{
|
|
1438
1438
|
"path": "docs/copy-inventory.md",
|
|
1439
1439
|
"mode": "0000644",
|
|
1440
|
-
"sha256": "
|
|
1440
|
+
"sha256": "8cb5bb088aae803e6889571712cc4439fc870c88e104b1bb81dbd5fd1f9b9bd3"
|
|
1441
1441
|
},
|
|
1442
1442
|
{
|
|
1443
1443
|
"path": "docs/copy-registry.json",
|
|
1444
1444
|
"mode": "0000644",
|
|
1445
|
-
"sha256": "
|
|
1445
|
+
"sha256": "18599cab2b3de43e33513c20cfaa241d3fe0545b861a27942d7f63d11f646750"
|
|
1446
1446
|
},
|
|
1447
1447
|
{
|
|
1448
1448
|
"path": "docs/design-contract.md",
|
|
@@ -3632,7 +3632,7 @@
|
|
|
3632
3632
|
{
|
|
3633
3633
|
"path": "modules/hall-ui/public/task-detail.js",
|
|
3634
3634
|
"mode": "0000644",
|
|
3635
|
-
"sha256": "
|
|
3635
|
+
"sha256": "e2c0702ec2eaa2c2d63de4d181f6118295a28e743f0df52722e5e717c3e96b72"
|
|
3636
3636
|
},
|
|
3637
3637
|
{
|
|
3638
3638
|
"path": "modules/hall-ui/public/task-lib.js",
|
|
@@ -4522,12 +4522,12 @@
|
|
|
4522
4522
|
{
|
|
4523
4523
|
"path": "package-lock.json",
|
|
4524
4524
|
"mode": "0000644",
|
|
4525
|
-
"sha256": "
|
|
4525
|
+
"sha256": "e96dcf6ef882c61be154cfcb5d902942a1cad182173d11b63c824fdfb87f9879"
|
|
4526
4526
|
},
|
|
4527
4527
|
{
|
|
4528
4528
|
"path": "package.json",
|
|
4529
4529
|
"mode": "0000644",
|
|
4530
|
-
"sha256": "
|
|
4530
|
+
"sha256": "b1b86eeccda0d20d4c9b739016bd727f4867fa59de6a9dc5526b24ba5a6c92dd"
|
|
4531
4531
|
},
|
|
4532
4532
|
{
|
|
4533
4533
|
"path": "public-docs/index.html",
|
|
@@ -5782,7 +5782,7 @@
|
|
|
5782
5782
|
{
|
|
5783
5783
|
"path": "scripts/gds/triage.js",
|
|
5784
5784
|
"mode": "0000644",
|
|
5785
|
-
"sha256": "
|
|
5785
|
+
"sha256": "1c43db4e69ff4966addd91314638b3b2a0c3f7c91f832f09e4a162e5fd0789aa"
|
|
5786
5786
|
},
|
|
5787
5787
|
{
|
|
5788
5788
|
"path": "scripts/gds/update-channel.js",
|
|
@@ -6172,7 +6172,7 @@
|
|
|
6172
6172
|
{
|
|
6173
6173
|
"path": "src/module-api.js",
|
|
6174
6174
|
"mode": "0000644",
|
|
6175
|
-
"sha256": "
|
|
6175
|
+
"sha256": "6d275b0dea8e96343483360f9429816f5b6b3a754d71e9ae0a3700481a22e0aa"
|
|
6176
6176
|
},
|
|
6177
6177
|
{
|
|
6178
6178
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7964,6 +7964,11 @@
|
|
|
7964
7964
|
"mode": "0000644",
|
|
7965
7965
|
"sha256": "211aa868b86a2b663f5525c3d2ddfa406498d608363d28c0786219d3c0b6e249"
|
|
7966
7966
|
},
|
|
7967
|
+
{
|
|
7968
|
+
"path": "tests/promote_goal_id.mjs",
|
|
7969
|
+
"mode": "0000644",
|
|
7970
|
+
"sha256": "24471b652054a82e985b86fdbbe2dc281114cf9829aed6c9b91348217e7f2460"
|
|
7971
|
+
},
|
|
7967
7972
|
{
|
|
7968
7973
|
"path": "tests/prose_edits.mjs",
|
|
7969
7974
|
"mode": "0000644",
|
|
@@ -1,10 +1,40 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: idea-triage
|
|
3
|
-
description: Daily walk through the Bongos idea_inbox —
|
|
3
|
+
description: Daily walk through the Bongos idea_inbox, which since task 1003071 holds only HOMELESS work — bugs and goal-bound captures now route to a task at filing time and never reach the inbox. For each open idea, prompt to promote (→ task, naming a goal), discard, or merge with another idea. Triggers when the user says "/idea-triage", "triage ideas", "review the inbox", "walk the idea inbox", or runs as a scheduled daily task. Metic+ rank only (Metic and Archon; not Xenos).
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
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
7
|
|
|
8
|
+
## The inbox is HOMELESS-ONLY now (task 1003071 / R13, goal 1000071)
|
|
9
|
+
|
|
10
|
+
**What changed.** Since R01 (task 1003065) a bug or goal-bound capture ROUTES at
|
|
11
|
+
filing time — it becomes a task in its goal, in one transaction, and never appears
|
|
12
|
+
here. The inbox is therefore no longer "everything anyone filed"; it holds only
|
|
13
|
+
**work with no home yet**. Expect it to be smaller, and read what IS in it as
|
|
14
|
+
work that genuinely needs a decision rather than a queue to clear.
|
|
15
|
+
|
|
16
|
+
**Two consequences for how you run this skill:**
|
|
17
|
+
|
|
18
|
+
1. **Every promote names a goal.** The promote step now asks for one, defaulted
|
|
19
|
+
for you, and `-` opts out explicitly. Take the default unless you disagree with
|
|
20
|
+
it. Opting out is legal but rarely right: a promote with no goal is **not**
|
|
21
|
+
goal-less — the server files it under the version's catch-all `<version> —
|
|
22
|
+
general` goal — and that bucket carries no done-when criteria, so since
|
|
23
|
+
[ADR 0183](../../../docs/adr/<redacted>.md) made criteria
|
|
24
|
+
close themselves off LINKED tasks, work parked there can never advance
|
|
25
|
+
anything. The general bucket used to be the silent default; it is now a choice
|
|
26
|
+
you type.
|
|
27
|
+
2. **The default is inherited, not guessed, when a hint exists.** Precedence is
|
|
28
|
+
the stored `suggested_goal_id` (written at capture precisely so triage inherits
|
|
29
|
+
the guess) FIRST, then the R07 suggester (`GET /goals/suggestions`). When the
|
|
30
|
+
suggester supplies it you are shown the score and the matched terms — check
|
|
31
|
+
them: token overlap can match an incidental word, so a low score with one
|
|
32
|
+
matched term is weak evidence, not a recommendation.
|
|
33
|
+
|
|
34
|
+
**If an idea should have routed at filing and didn't**, that is worth noticing
|
|
35
|
+
rather than just promoting past: it usually means the filer named no goal, and the
|
|
36
|
+
capture-time suggester (R08/R09/R11 surfaces) is where the fix belongs.
|
|
37
|
+
|
|
8
38
|
> **This skill is no longer the only way to triage.** Since task 1002447 a Metic+ builder can promote, discard and merge from the **hall** — the Home "Needs your attention" card's idea rows each carry a *Triage* verb that opens the record overlay, where all three forms live. No Claude session involved. What this skill adds over the hall is the **parallel development pass** (Step 1.5) — an idea fleshed out into open questions/risks before the verdict — so reach for it when the queue needs judgment, and use the hall when it just needs clearing.
|
|
9
39
|
|
|
10
40
|
## Rank gate — Metic+ only
|
|
@@ -70,7 +100,7 @@ node scripts/gds/triage.js --development "${TMPDIR:-/tmp}/otb-idea-development.j
|
|
|
70
100
|
|
|
71
101
|
`triage.js` prints each idea's developed block above its verdict prompt. It never *generates* the analysis (that's this skill's subagent fan-out) — it only displays what the cache holds, and runs exactly as before when no cache is present.
|
|
72
102
|
|
|
73
|
-
> **Not in this cut (follow-up leg).** The next step is to let the Archon push the developed `open_questions` *back to the submitter* — a round-trip in the Discord `#ideas` thread / DM before promotion — so the submitter says how they'd want it integrated. That needs the Discord bot interaction + idea↔thread linking (idea
|
|
103
|
+
> **Not in this cut (follow-up leg).** The next step is to let the Archon push the developed `open_questions` *back to the submitter* — a round-trip in the Discord `#ideas` thread / DM before promotion — so the submitter says how they'd want it integrated. That needs the Discord bot interaction + idea↔thread linking (idea `#216` / [ADR 0033](../../../docs/adr/<redacted>.md)) and is tracked separately. For now the open questions are answered by the Archon in-session.
|
|
74
104
|
|
|
75
105
|
**Step 2 — walk the list, idea by idea.**
|
|
76
106
|
|
package/docs/copy-inventory.md
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
> Deterministic on a given tree — regenerate and commit rather than hand-editing.
|
|
5
5
|
> Machine-readable twin: [`copy-registry.json`](copy-registry.json).
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**1339 strings** across 3 surfaces · 1188 distinct pieces of copy · 1339 certain, 0 heuristic · 121 shown in more than one place.
|
|
8
8
|
|
|
9
9
|
## Surfaces
|
|
10
10
|
|
|
11
11
|
| surface | lives at | files | strings |
|
|
12
12
|
|---|---|---|---|
|
|
13
|
-
| `builders-hall` | `modules/hall-ui/public` | 61 |
|
|
13
|
+
| `builders-hall` | `modules/hall-ui/public` | 61 | 1055 |
|
|
14
14
|
| `status-dashboard` | `modules/status-ui/public` | 4 | 98 |
|
|
15
15
|
| `landing` | `public-landing` | 2 | 186 |
|
|
16
16
|
|
|
@@ -78,7 +78,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
78
78
|
| `7775fec0fa85` | 2x | Could not load sessions. | modules/hall-ui/public/sessions.js:168, modules/hall-ui/public/sessions.js:207 |
|
|
79
79
|
| … | | _81 more in the JSON registry_ | |
|
|
80
80
|
|
|
81
|
-
## `builders-hall` —
|
|
81
|
+
## `builders-hall` — 1055 strings
|
|
82
82
|
|
|
83
83
|
| id | conf | text | where |
|
|
84
84
|
|---|---|---|---|
|
|
@@ -326,7 +326,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
326
326
|
| `7f600651bbf1` | certain | Couldn’t load your projects right now. | `modules/hall-ui/public/my-projects.js:115` |
|
|
327
327
|
| `38827a954f3b` | certain | Create a custom rank first. | `modules/hall-ui/public/government.js:397` |
|
|
328
328
|
| `dd8ead7adc9a` | certain | Create rank | `modules/hall-ui/public/government.html:62` |
|
|
329
|
-
| `d7beb841395e` | certain | Create task | `modules/hall-ui/public/task-detail.js:
|
|
329
|
+
| `d7beb841395e` | certain | Create task | `modules/hall-ui/public/task-detail.js:354` |
|
|
330
330
|
| `533f24219487` | certain | Create your | `modules/hall-ui/public/builders.js:984` |
|
|
331
331
|
| `e91a4d2e3e1c` | certain | Created rank {…}. | `modules/hall-ui/public/government.js:380` |
|
|
332
332
|
| `5968d13a9dbe` | certain | Criteria met — awaiting review | `modules/hall-ui/public/goals-page.js:341` |
|
|
@@ -364,7 +364,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
364
364
|
| `854a21d2eb39` | certain | Eight achievements recognize sustained contribution. Earned ones are highlighted; the rest show what’s left to | `modules/hall-ui/public/hall-widgets.js:197` |
|
|
365
365
|
| `58d381535285` | certain | Engagement reports | `modules/hall-ui/public/watch.html:120` |
|
|
366
366
|
| `35d1ae09a6b3` | certain | Enter a project URL. | `modules/hall-ui/public/my-projects.js:122` |
|
|
367
|
-
| `bd7a40639a1c` | certain | Estimate (min) | `modules/hall-ui/public/task-detail.js:
|
|
367
|
+
| `bd7a40639a1c` | certain | Estimate (min) | `modules/hall-ui/public/task-detail.js:349` |
|
|
368
368
|
| `c15d1f0ddfb6` | certain | Estimation accuracy | `modules/hall-ui/public/watch.html:133` |
|
|
369
369
|
| `44f928d13699` | certain | Event sounds | `modules/hall-ui/public/settings.html:282` |
|
|
370
370
|
| `8690c6424585` | certain | Every builder in {{worldName}} — rank, {{currency}}, karma, and active claims. | `modules/hall-ui/public/watch.html:61` |
|
|
@@ -387,7 +387,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
387
387
|
| `b9267033b1f1` | certain | Flag this copy | `modules/hall-ui/public/copy-desk.html:87` |
|
|
388
388
|
| `448d00771dc8` | certain | Flagged. It is in the queue. | `modules/hall-ui/public/copy-desk.js:281` |
|
|
389
389
|
| `617f13fa3fdb` | certain | Flip the switch. | `modules/hall-ui/public/builders.js:984` |
|
|
390
|
-
| `42cee5cb3584` | certain | Fold this idea into # | `modules/hall-ui/public/task-detail.js:
|
|
390
|
+
| `42cee5cb3584` | certain | Fold this idea into # | `modules/hall-ui/public/task-detail.js:444` |
|
|
391
391
|
| `647bcfcfdb03` | certain | Fresh CLI token issued. Paste it in your terminal: | `modules/hall-ui/public/settings.js:885` |
|
|
392
392
|
| `55c71d76a94c` | certain | From creation to production — and the exact step where {{currency}} are awarded (at | `modules/hall-ui/public/diagrams.html:61` |
|
|
393
393
|
| `a025080147a1` | certain | Full Idea | `modules/hall-ui/public/government.js:669` |
|
|
@@ -490,7 +490,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
490
490
|
| `0f9f86c556a2` | certain | Loading the primer… | `modules/hall-ui/public/primer.html:55` |
|
|
491
491
|
| `ceec2218efe3` | certain | Loading the roadmap… | `modules/hall-ui/public/roadmap.html:24` |
|
|
492
492
|
| `ceec2218efe3` | certain | Loading the roadmap… | `modules/hall-ui/public/roadmap.html:29` |
|
|
493
|
-
| `d28d321e8a1e` | certain | Loading versions… | `modules/hall-ui/public/task-detail.js:
|
|
493
|
+
| `d28d321e8a1e` | certain | Loading versions… | `modules/hall-ui/public/task-detail.js:307` |
|
|
494
494
|
| `3c43f157035e` | certain | Loading your goals… | `modules/hall-ui/public/goals.js:95` |
|
|
495
495
|
| `7a239a17afc8` | certain | Loading your preferences… | `modules/hall-ui/public/settings.html:26` |
|
|
496
496
|
| `04df1bd30129` | certain | Loading your rank… | `modules/hall-ui/public/ranks.html:23` |
|
|
@@ -564,7 +564,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
564
564
|
| `2c19f41704b9` | certain | Not yet available — {{worldName}} | `modules/hall-ui/public/not-ready.html:6` |
|
|
565
565
|
| `2c19f41704b9` | certain | Not yet available — {{worldName}} | `modules/hall-ui/public/not-ready.html:6` |
|
|
566
566
|
| `571b9b22dbda` | certain | Not yet ready — blocked by dependencies or awaiting promotion. These become claimable when their blockers ship | `modules/hall-ui/public/work.html:87` |
|
|
567
|
-
| `0c59d118db9b` | certain | Note (optional) | `modules/hall-ui/public/task-detail.js:
|
|
567
|
+
| `0c59d118db9b` | certain | Note (optional) | `modules/hall-ui/public/task-detail.js:447` |
|
|
568
568
|
| `95f8f8a4b288` | certain | Nothing flagged here. That is either good news or an unread queue — the duplicates below are the cheapest plac | `modules/hall-ui/public/copy-desk.js:172` |
|
|
569
569
|
| `fec5d78497e8` | certain | Nothing in the backlog right now. | `modules/hall-ui/public/work.js:953` |
|
|
570
570
|
| `18ce0723be0d` | certain | Nothing is waiting — no held changes need your approval. | `modules/hall-ui/public/gate.js:188` |
|
|
@@ -642,7 +642,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
642
642
|
| `26e6c9879035` | certain | Reading the diagrams | `modules/hall-ui/public/diagrams.html:48` |
|
|
643
643
|
| `81cd2cdf954d` | certain | Reading the request… | `modules/hall-ui/public/pair.html:23` |
|
|
644
644
|
| `5b581cca590f` | certain | Ready — tasks to do | `modules/hall-ui/public/work.html:53` |
|
|
645
|
-
| `9ea556e7b78b` | certain | Reason (optional) | `modules/hall-ui/public/task-detail.js:
|
|
645
|
+
| `9ea556e7b78b` | certain | Reason (optional) | `modules/hall-ui/public/task-detail.js:498` |
|
|
646
646
|
| `7df3fd4ebf41` | certain | Reason (shown to the builder) | `modules/hall-ui/public/harbor.js:146` |
|
|
647
647
|
| `06cbba1440b1` | certain | Rebase owed — new claims paused | `modules/hall-ui/public/builders.js:752` |
|
|
648
648
|
| `58fb6fe4b47e` | certain | Recent history could not be loaded right now. | `modules/hall-ui/public/work.js:1052` |
|
|
@@ -974,7 +974,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
974
974
|
| `06aab18fc3b5` | certain | holds the repo. Follow the numbered path to see how a credential-less box gets its work to prod. This is the l | `modules/hall-ui/public/diagrams.html:83` |
|
|
975
975
|
| `17ab8db79e1f` | certain | holds the worker, the code, and your local memory; the | `modules/hall-ui/public/diagrams.html:83` |
|
|
976
976
|
| `7d5cbe3d81e4` | certain | how long a vote stays open | `modules/hall-ui/public/government.html:40` |
|
|
977
|
-
| `dacb901c4f10` | certain | idea number | `modules/hall-ui/public/task-detail.js:
|
|
977
|
+
| `dacb901c4f10` | certain | idea number | `modules/hall-ui/public/task-detail.js:445` |
|
|
978
978
|
| `b2b399fdd6a4` | certain | in progress | `modules/hall-ui/public/goals-page.js:423` |
|
|
979
979
|
| `b2b399fdd6a4` | certain | in progress | `modules/hall-ui/public/roadmap.js:108` |
|
|
980
980
|
| `b2b399fdd6a4` | certain | in progress | `modules/hall-ui/public/roadmap.js:194` |
|
|
@@ -987,6 +987,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
987
987
|
| `c767b3e07826` | certain | is only the screen; the | `modules/hall-ui/public/diagrams.html:83` |
|
|
988
988
|
| `aa530bd15088` | certain | itself). The registry stays the single source of truth for what the hall shows; a fitness check (scripts/gds/f | `modules/hall-ui/public/index.html:44` |
|
|
989
989
|
| `d46875348b9c` | certain | ladder · read-only | `modules/hall-ui/public/government.js:116` |
|
|
990
|
+
| `2ce7738b1eb0` | certain | loading goals… | `modules/hall-ui/public/task-detail.js:371` |
|
|
990
991
|
| `8c22f0ee5edb` | certain | looked at | `modules/hall-ui/public/copy-desk.js:114` |
|
|
991
992
|
| `754bdec368ed` | certain | lowest credits | `modules/hall-ui/public/hall-widgets.js:136` |
|
|
992
993
|
| `cffba8409070` | certain | manage its visibility in Settings → Cross-project record | `modules/hall-ui/public/profile.js:200` |
|
|
@@ -1053,8 +1054,8 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1053
1054
|
| `1651cb1876f5` | certain | who sits on the board | `modules/hall-ui/public/government.html:40` |
|
|
1054
1055
|
| `4109de2e0bb9` | certain | whole surface | `modules/hall-ui/public/copy-desk.js:88` |
|
|
1055
1056
|
| `4109de2e0bb9` | certain | whole surface | `modules/hall-ui/public/copy-desk.js:251` |
|
|
1056
|
-
| `143f77e2b782` | certain | why these are the same | `modules/hall-ui/public/task-detail.js:
|
|
1057
|
-
| `ec7c228cb4ce` | certain | why this isn’t worth doing | `modules/hall-ui/public/task-detail.js:
|
|
1057
|
+
| `143f77e2b782` | certain | why these are the same | `modules/hall-ui/public/task-detail.js:448` |
|
|
1058
|
+
| `ec7c228cb4ce` | certain | why this isn’t worth doing | `modules/hall-ui/public/task-detail.js:499` |
|
|
1058
1059
|
| `66d979d0db89` | certain | your vote | `modules/hall-ui/public/government.js:691` |
|
|
1059
1060
|
| `7e2f2aac0645` | certain | {…} You always see your own numbers here. | `modules/hall-ui/public/settings.js:580` |
|
|
1060
1061
|
| `43b99dd1b755` | certain | {…} You can also ask in Discord #support. | `modules/hall-ui/public/box-panel.js:358` |
|
|
@@ -1104,6 +1105,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1104
1105
|
| `cbd220dd9184` | certain | — a later push must be re-approved. | `modules/hall-ui/public/gate.js:87` |
|
|
1105
1106
|
| `a6ae9cbdee30` | certain | — all builders — | `modules/hall-ui/public/builders.js:1692` |
|
|
1106
1107
|
| `729d4ce22b99` | certain | — any — | `modules/hall-ui/public/hall-widgets.js:132` |
|
|
1108
|
+
| `9ac518dfa95e` | certain | — none (the version's general bucket) — | `modules/hall-ui/public/task-detail.js:302` |
|
|
1107
1109
|
| `da9962dc1729` | certain | — or open any diagram fullscreen. | `modules/hall-ui/public/diagrams.html:42` |
|
|
1108
1110
|
| `d4ff85363f89` | certain | — so if Claude Desktop then says | `modules/hall-ui/public/box-panel.js:160` |
|
|
1109
1111
|
| `e5c5e94152b3` | certain | — that's all the tooling you need up front. | `modules/hall-ui/public/builders.js:1051` |
|
|
@@ -1122,7 +1124,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1122
1124
|
| `7926e30955d2` | certain | ← Back to goal | `modules/hall-ui/public/goals.html:163` |
|
|
1123
1125
|
| `7926e30955d2` | certain | ← Back to goal | `modules/hall-ui/public/goals.html:175` |
|
|
1124
1126
|
| `1e7cf7b1e64e` | certain | ← Newer | `modules/hall-ui/public/sessions.html:56` |
|
|
1125
|
-
| `2180e0ecf138` | certain | → no idea # | `modules/hall-ui/public/task-detail.js:
|
|
1127
|
+
| `2180e0ecf138` | certain | → no idea # | `modules/hall-ui/public/task-detail.js:466` |
|
|
1126
1128
|
| `d5d76f1c0dfe` | certain | ○ reserved | `modules/hall-ui/public/diagrams.html:53` |
|
|
1127
1129
|
| `97b0773e72a5` | certain | ● LIVE | `modules/hall-ui/public/diagrams.html:51` |
|
|
1128
1130
|
| `1375334c77f6` | certain | ⚠ This box is running | `modules/hall-ui/public/box-panel.js:202` |
|
package/docs/copy-registry.json
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"generator": "scripts/gds/copy-inventory.js",
|
|
4
4
|
"goal": 1000074,
|
|
5
5
|
"counts": {
|
|
6
|
-
"strings":
|
|
7
|
-
"unique_ids":
|
|
8
|
-
"certain":
|
|
6
|
+
"strings": 1339,
|
|
7
|
+
"unique_ids": 1188,
|
|
8
|
+
"certain": 1339,
|
|
9
9
|
"heuristic": 0,
|
|
10
10
|
"duplicate_ids": 121
|
|
11
11
|
},
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"builders-hall": {
|
|
14
14
|
"at": "modules/hall-ui/public",
|
|
15
15
|
"files": 61,
|
|
16
|
-
"strings":
|
|
16
|
+
"strings": 1055
|
|
17
17
|
},
|
|
18
18
|
"status-dashboard": {
|
|
19
19
|
"at": "modules/status-ui/public",
|
|
@@ -3481,7 +3481,7 @@
|
|
|
3481
3481
|
"surface": "builders-hall",
|
|
3482
3482
|
"text": "Create task",
|
|
3483
3483
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
3484
|
-
"line":
|
|
3484
|
+
"line": 354,
|
|
3485
3485
|
"origin": "js-markup",
|
|
3486
3486
|
"confidence": "certain"
|
|
3487
3487
|
},
|
|
@@ -3823,7 +3823,7 @@
|
|
|
3823
3823
|
"surface": "builders-hall",
|
|
3824
3824
|
"text": "Estimate (min)",
|
|
3825
3825
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
3826
|
-
"line":
|
|
3826
|
+
"line": 349,
|
|
3827
3827
|
"origin": "js-markup",
|
|
3828
3828
|
"confidence": "certain"
|
|
3829
3829
|
},
|
|
@@ -4030,7 +4030,7 @@
|
|
|
4030
4030
|
"surface": "builders-hall",
|
|
4031
4031
|
"text": "Fold this idea into #",
|
|
4032
4032
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
4033
|
-
"line":
|
|
4033
|
+
"line": 444,
|
|
4034
4034
|
"origin": "js-markup",
|
|
4035
4035
|
"confidence": "certain"
|
|
4036
4036
|
},
|
|
@@ -4957,7 +4957,7 @@
|
|
|
4957
4957
|
"surface": "builders-hall",
|
|
4958
4958
|
"text": "Loading versions…",
|
|
4959
4959
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
4960
|
-
"line":
|
|
4960
|
+
"line": 307,
|
|
4961
4961
|
"origin": "js-markup",
|
|
4962
4962
|
"confidence": "certain"
|
|
4963
4963
|
},
|
|
@@ -5623,7 +5623,7 @@
|
|
|
5623
5623
|
"surface": "builders-hall",
|
|
5624
5624
|
"text": "Note (optional)",
|
|
5625
5625
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
5626
|
-
"line":
|
|
5626
|
+
"line": 447,
|
|
5627
5627
|
"origin": "js-markup",
|
|
5628
5628
|
"confidence": "certain"
|
|
5629
5629
|
},
|
|
@@ -6325,7 +6325,7 @@
|
|
|
6325
6325
|
"surface": "builders-hall",
|
|
6326
6326
|
"text": "Reason (optional)",
|
|
6327
6327
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
6328
|
-
"line":
|
|
6328
|
+
"line": 498,
|
|
6329
6329
|
"origin": "js-markup",
|
|
6330
6330
|
"confidence": "certain"
|
|
6331
6331
|
},
|
|
@@ -9313,7 +9313,7 @@
|
|
|
9313
9313
|
"surface": "builders-hall",
|
|
9314
9314
|
"text": "idea number",
|
|
9315
9315
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
9316
|
-
"line":
|
|
9316
|
+
"line": 445,
|
|
9317
9317
|
"origin": "js-markup",
|
|
9318
9318
|
"confidence": "certain"
|
|
9319
9319
|
},
|
|
@@ -9425,6 +9425,15 @@
|
|
|
9425
9425
|
"origin": "js-markup",
|
|
9426
9426
|
"confidence": "certain"
|
|
9427
9427
|
},
|
|
9428
|
+
{
|
|
9429
|
+
"id": "2ce7738b1eb0",
|
|
9430
|
+
"surface": "builders-hall",
|
|
9431
|
+
"text": "loading goals…",
|
|
9432
|
+
"file": "modules/hall-ui/public/task-detail.js",
|
|
9433
|
+
"line": 371,
|
|
9434
|
+
"origin": "js-markup",
|
|
9435
|
+
"confidence": "certain"
|
|
9436
|
+
},
|
|
9428
9437
|
{
|
|
9429
9438
|
"id": "8c22f0ee5edb",
|
|
9430
9439
|
"surface": "builders-hall",
|
|
@@ -10024,7 +10033,7 @@
|
|
|
10024
10033
|
"surface": "builders-hall",
|
|
10025
10034
|
"text": "why these are the same",
|
|
10026
10035
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
10027
|
-
"line":
|
|
10036
|
+
"line": 448,
|
|
10028
10037
|
"origin": "js-markup",
|
|
10029
10038
|
"confidence": "certain"
|
|
10030
10039
|
},
|
|
@@ -10033,7 +10042,7 @@
|
|
|
10033
10042
|
"surface": "builders-hall",
|
|
10034
10043
|
"text": "why this isn’t worth doing",
|
|
10035
10044
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
10036
|
-
"line":
|
|
10045
|
+
"line": 499,
|
|
10037
10046
|
"origin": "js-markup",
|
|
10038
10047
|
"confidence": "certain"
|
|
10039
10048
|
},
|
|
@@ -10478,6 +10487,15 @@
|
|
|
10478
10487
|
"origin": "js-markup",
|
|
10479
10488
|
"confidence": "certain"
|
|
10480
10489
|
},
|
|
10490
|
+
{
|
|
10491
|
+
"id": "9ac518dfa95e",
|
|
10492
|
+
"surface": "builders-hall",
|
|
10493
|
+
"text": "— none (the version's general bucket) —",
|
|
10494
|
+
"file": "modules/hall-ui/public/task-detail.js",
|
|
10495
|
+
"line": 302,
|
|
10496
|
+
"origin": "js-markup",
|
|
10497
|
+
"confidence": "certain"
|
|
10498
|
+
},
|
|
10481
10499
|
{
|
|
10482
10500
|
"id": "da9962dc1729",
|
|
10483
10501
|
"surface": "builders-hall",
|
|
@@ -10645,7 +10663,7 @@
|
|
|
10645
10663
|
"surface": "builders-hall",
|
|
10646
10664
|
"text": "→ no idea #",
|
|
10647
10665
|
"file": "modules/hall-ui/public/task-detail.js",
|
|
10648
|
-
"line":
|
|
10666
|
+
"line": 466,
|
|
10649
10667
|
"origin": "js-text-assign",
|
|
10650
10668
|
"confidence": "certain"
|
|
10651
10669
|
},
|
|
@@ -254,6 +254,55 @@
|
|
|
254
254
|
+ '</div>';
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
// task 1003071 (R13) — the goal picker's options, same precedence as the CLI
|
|
258
|
+
// triage path: the STORED hint (idea.suggested_goal_id, written by
|
|
259
|
+
// inbox.captureIdea precisely so triage inherits the guess) wins; the R07
|
|
260
|
+
// suggester only fills in when there is no hint. Returns option HTML.
|
|
261
|
+
async function goalOptionsFor(versionId) {
|
|
262
|
+
// Number() so a junk stored value cannot reach the DOM as value="NaN" and
|
|
263
|
+
// then be POSTed as a goal id.
|
|
264
|
+
var raw = Number(idea.suggested_goal_id);
|
|
265
|
+
var hint = Number.isFinite(raw) && raw > 0 ? raw : null;
|
|
266
|
+
var suggestions = [];
|
|
267
|
+
// The suggester is a convenience, so an outage degrades to "no suggestions"
|
|
268
|
+
// and must not stop a triage verdict — the hint and the opt-out below are
|
|
269
|
+
// both still reachable without it.
|
|
270
|
+
try {
|
|
271
|
+
var q = '?version=' + encodeURIComponent(versionId)
|
|
272
|
+
+ '&title=' + encodeURIComponent(idea.title || '')
|
|
273
|
+
+ '&body=' + encodeURIComponent(String(idea.body_md || '').slice(0, 2000))
|
|
274
|
+
+ '&limit=5';
|
|
275
|
+
// softAuth: a cosmetic suggestion list must not navigate the builder away
|
|
276
|
+
// from a half-filled promote form. The submit re-auths if the session died.
|
|
277
|
+
var res = await getJSON(API + '/goals/suggestions' + q, { softAuth: true });
|
|
278
|
+
suggestions = (res && res.suggestions) || [];
|
|
279
|
+
} catch (_) { suggestions = []; }
|
|
280
|
+
var opts = [];
|
|
281
|
+
var seen = {};
|
|
282
|
+
if (hint != null) {
|
|
283
|
+
seen[hint] = true;
|
|
284
|
+
opts.push('<option value="' + hint + '" selected>goal ' + hint + ' — carried from the filing</option>');
|
|
285
|
+
}
|
|
286
|
+
suggestions.forEach(function (g, i) {
|
|
287
|
+
if (seen[g.id]) return;
|
|
288
|
+
seen[g.id] = true;
|
|
289
|
+
var why = (g.matched_terms || []).join(', ');
|
|
290
|
+
// The score + matched terms are shown on purpose: token overlap can match
|
|
291
|
+
// an incidental word, so the operator judges the suggestion rather than
|
|
292
|
+
// trusting it (task 1003062's stated limit).
|
|
293
|
+
opts.push('<option value="' + esc(String(g.id)) + '"' + (hint == null && i === 0 ? ' selected' : '') + '>'
|
|
294
|
+
+ esc(String(g.id)) + ' — ' + esc(g.title || '') + (why ? ' (matched: ' + esc(why) + ')' : '') + '</option>');
|
|
295
|
+
});
|
|
296
|
+
// Always offer the opt-out, and make it the fallback when nothing was found.
|
|
297
|
+
// Sending no goal_id does not create a goal-less task — db.createTask files
|
|
298
|
+
// it under the version's catch-all "general" goal — so the label names where
|
|
299
|
+
// it actually lands. That bucket carries no done-when criteria, which is why
|
|
300
|
+
// it should be a choice rather than the silent default it used to be.
|
|
301
|
+
opts.push('<option value=""' + (opts.length === 0 ? ' selected' : '')
|
|
302
|
+
+ '>— none (the version\'s general bucket) —</option>');
|
|
303
|
+
return opts.join('');
|
|
304
|
+
}
|
|
305
|
+
|
|
257
306
|
async function renderPromote() {
|
|
258
307
|
box.innerHTML = '<p class="deeplink-card__note">Loading versions…</p>';
|
|
259
308
|
// Both dead ends offer the way back through the same delegated [data-go]
|
|
@@ -283,6 +332,10 @@
|
|
|
283
332
|
// CHECKed 1-5, so clamp rather than let an out-of-range value match no
|
|
284
333
|
// option and leave the browser silently defaulting to P1.
|
|
285
334
|
var pri = Math.min(5, Math.max(1, Number(idea.suggested_priority) || 3));
|
|
335
|
+
// Fetched for the PREFERRED version; the change listener below re-fetches,
|
|
336
|
+
// because goals belong to a version and showing another version's goals
|
|
337
|
+
// would offer a home the server would reject.
|
|
338
|
+
var goalOptions = await goalOptionsFor(preferred);
|
|
286
339
|
var priOptions = [1, 2, 3, 4, 5].map(function (p) {
|
|
287
340
|
return '<option value="' + p + '"' + (p === pri ? ' selected' : '') + '>P' + p + '</option>';
|
|
288
341
|
}).join('');
|
|
@@ -291,6 +344,7 @@
|
|
|
291
344
|
+ '<label class="td-field td-field--wide"><span>Title</span>'
|
|
292
345
|
+ '<input type="text" name="title" maxlength="' + TITLE_MAX + '" required value="' + esc(idea.title || '') + '"></label>'
|
|
293
346
|
+ '<label class="td-field td-field--wide"><span>Version</span><select name="version">' + options + '</select></label>'
|
|
347
|
+
+ '<label class="td-field td-field--wide"><span>Goal</span><select name="goal" data-goal-select>' + goalOptions + '</select></label>'
|
|
294
348
|
+ '<label class="td-field"><span>Priority</span><select name="priority">' + priOptions + '</select></label>'
|
|
295
349
|
+ '<label class="td-field"><span>Estimate (min)</span>'
|
|
296
350
|
+ '<input type="number" name="est" min="0" max="100000" step="5" placeholder="—"></label>'
|
|
@@ -304,6 +358,23 @@
|
|
|
304
358
|
ev.preventDefault();
|
|
305
359
|
submitPromote(ev.target);
|
|
306
360
|
});
|
|
361
|
+
// Re-fetch the goal list when the version changes (task 1003071). Two quick
|
|
362
|
+
// changes race — the fetches can resolve out of order — so only the newest
|
|
363
|
+
// request is allowed to write, and only it re-enables the control.
|
|
364
|
+
var vsel = box.querySelector('select[name="version"]');
|
|
365
|
+
var gsel = box.querySelector('[data-goal-select]');
|
|
366
|
+
if (vsel && gsel) {
|
|
367
|
+
var goalFetch = 0;
|
|
368
|
+
vsel.addEventListener('change', async function () {
|
|
369
|
+
var mine = ++goalFetch;
|
|
370
|
+
gsel.disabled = true;
|
|
371
|
+
gsel.innerHTML = '<option value="">loading goals…</option>';
|
|
372
|
+
var html = await goalOptionsFor(vsel.value);
|
|
373
|
+
if (mine !== goalFetch) return;
|
|
374
|
+
gsel.innerHTML = html;
|
|
375
|
+
gsel.disabled = false;
|
|
376
|
+
});
|
|
377
|
+
}
|
|
307
378
|
}
|
|
308
379
|
|
|
309
380
|
async function submitPromote(form) {
|
|
@@ -325,6 +396,10 @@
|
|
|
325
396
|
source: 'idea_inbox',
|
|
326
397
|
source_ref: 'idea:' + idea.id,
|
|
327
398
|
};
|
|
399
|
+
// task 1003071 (R13): the whole point — a triaged promotion NAMES its goal.
|
|
400
|
+
// Empty means the operator chose the version's general bucket, which the
|
|
401
|
+
// server applies itself when goal_id is absent.
|
|
402
|
+
if (form.goal && form.goal.value !== '') payload.goal_id = Number(form.goal.value);
|
|
328
403
|
if (form.est.value !== '') payload.est_minutes = Number(form.est.value);
|
|
329
404
|
if (form.credits.value !== '') payload.credits_reward = Number(form.credits.value);
|
|
330
405
|
// 'criterion-proposal' is an idea-only kind; everything else is shared with
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.290",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.290",
|
|
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.19.
|
|
3
|
+
"version": "1.19.290",
|
|
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/scripts/gds/triage.js
CHANGED
|
@@ -151,11 +151,80 @@ async function patchIdea(id, patch) {
|
|
|
151
151
|
return r.data?.idea;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
// resolveGoalDefault — what goal should a triaged promotion land in? (task
|
|
155
|
+
// 1003071 / R13.)
|
|
156
|
+
//
|
|
157
|
+
// PRECEDENCE IS DELIBERATE:
|
|
158
|
+
// 1. `idea.suggested_goal_id` — the STORED hint. inbox.captureIdea writes it
|
|
159
|
+
// even when the matrix declined to route, precisely so "/idea-triage
|
|
160
|
+
// inherits the guess instead of re-deriving it" (core_222, its own comment).
|
|
161
|
+
// A human or a filing surface already named this goal; a heuristic must not
|
|
162
|
+
// overrule that.
|
|
163
|
+
// 2. the R07 suggester (GET /goals/suggestions) — only when there is no hint.
|
|
164
|
+
// Best-scoring candidate, and its score/matched_terms are shown so the
|
|
165
|
+
// operator can see WHY before accepting.
|
|
166
|
+
// Returns { goalId, label, source } — goalId null when neither yields anything.
|
|
167
|
+
// The caller prints `source` as provenance, because "the suggester guessed this"
|
|
168
|
+
// and "the filing already named this" deserve different trust. Never throws: a
|
|
169
|
+
// suggester outage degrades to "no default", it does not block a triage verdict.
|
|
170
|
+
//
|
|
171
|
+
// `client` is injectable so the precedence can be driven end-to-end in a test
|
|
172
|
+
// rather than pattern-matched in the source. (Task 1003102 shipped a handler
|
|
173
|
+
// whose collaborators were not injectable; its tests read the source, passed,
|
|
174
|
+
// and the route 500'd on every call. Not repeating that.)
|
|
175
|
+
async function resolveGoalDefault(idea, versionId, { client = cliClient } = {}) {
|
|
176
|
+
// Guarded: a junk stored value must not become goalId NaN and then a goal_id
|
|
177
|
+
// the server rejects with 400 on every retry.
|
|
178
|
+
const hint = Number(idea.suggested_goal_id);
|
|
179
|
+
if (Number.isInteger(hint) && hint > 0) {
|
|
180
|
+
return { goalId: hint, label: `goal ${hint}`, source: 'hint' };
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const api = await client();
|
|
184
|
+
// The GENERATED typed method (regenerated when R07 landed), not the generic
|
|
185
|
+
// request() escape hatch — so a path or param rename reds the client build.
|
|
186
|
+
const r = await api.goals.getGoalsSuggestions({
|
|
187
|
+
query: { version: versionId, title: idea.title || '', body: (idea.body_md || '').slice(0, 2000), limit: 3 },
|
|
188
|
+
});
|
|
189
|
+
const top = r.ok && r.data && Array.isArray(r.data.suggestions) ? r.data.suggestions[0] : null;
|
|
190
|
+
if (!top) return { goalId: null, label: null, source: 'none' };
|
|
191
|
+
const why = (top.matched_terms || []).join('/');
|
|
192
|
+
return { goalId: Number(top.id), label: `${top.id} ${trim(top.title, 44)} (score ${top.score}${why ? ' · ' + why : ''})`, source: 'suggester' };
|
|
193
|
+
} catch (_) {
|
|
194
|
+
return { goalId: null, label: null, source: 'unavailable' };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// What the operator's answer to the goal prompt means. Blank ACCEPTS the shown
|
|
199
|
+
// default — declining has to be said ('-' or 'none'), because silently dropping a
|
|
200
|
+
// real default on a bare Enter is how work ends up in the general bucket by
|
|
201
|
+
// accident. Declining is legal: the server then applies the catch-all itself.
|
|
202
|
+
function applyGoalAnswer(defaultGoalId, rawAnswer) {
|
|
203
|
+
const answer = String(rawAnswer == null ? '' : rawAnswer).trim();
|
|
204
|
+
if (answer === '') return { goalId: defaultGoalId, warn: null };
|
|
205
|
+
if (answer === '-' || answer.toLowerCase() === 'none') return { goalId: null, warn: null };
|
|
206
|
+
const parsed = Number(answer);
|
|
207
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
208
|
+
return { goalId: null, warn: "not a goal id — falling back to the version's general bucket" };
|
|
209
|
+
}
|
|
210
|
+
return { goalId: parsed, warn: null };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function createTask({ versionId, title, description, priority, sourceRef, goalId = null }) {
|
|
214
|
+
// (The old note here said POST /api/gds/tasks does not accept 'kind'. That is
|
|
215
|
+
// no longer true — routes/tasks.js takes it, and the hall promote path already
|
|
216
|
+
// sends it. Left un-sent from THIS path deliberately: changing what kind a
|
|
217
|
+
// triaged promotion lands with is a behaviour change task R13 was not asked to
|
|
218
|
+
// make, and the server classifier still runs. Corrected rather than deleted so
|
|
219
|
+
// the next reader does not re-derive it.)
|
|
220
|
+
//
|
|
221
|
+
// task 1003071 (R13): goal_id is the point of this task. Omitting it does NOT
|
|
222
|
+
// leave the task goal-less — db.createTask (modules/lifecycle/db.js) defaults a
|
|
223
|
+
// goal-less create into the version's catch-all "<version> — general" goal. That
|
|
224
|
+
// bucket is the actual defect: it carries no done-when criteria, so since ADR
|
|
225
|
+
// 0183 made criteria close themselves off LINKED tasks, work parked there can
|
|
226
|
+
// never advance anything, and it does not read as real goal work on the board.
|
|
227
|
+
// Naming the goal here is what stops a triaged promotion falling into it.
|
|
159
228
|
const body = {
|
|
160
229
|
version_id: versionId,
|
|
161
230
|
title,
|
|
@@ -165,6 +234,7 @@ async function createTask({ versionId, title, description, priority, sourceRef }
|
|
|
165
234
|
source: 'idea_inbox',
|
|
166
235
|
source_ref: sourceRef ?? null,
|
|
167
236
|
};
|
|
237
|
+
if (goalId != null) body.goal_id = Number(goalId);
|
|
168
238
|
const api = await cliClient();
|
|
169
239
|
const r = await api.tasks.postTasks({ body });
|
|
170
240
|
if (!r.ok) {
|
|
@@ -296,7 +366,7 @@ async function main() {
|
|
|
296
366
|
}
|
|
297
367
|
|
|
298
368
|
if (ans === 'p' || ans === 'promote') {
|
|
299
|
-
// Promote:
|
|
369
|
+
// Promote: version, priority, title, goal — each defaulted from the idea.
|
|
300
370
|
const defaultVersion = idea.suggested_version || 'PMS-V2';
|
|
301
371
|
const versionAns = (await prompt(rl, ` version [${defaultVersion}]: `)).trim();
|
|
302
372
|
const versionId = versionAns || defaultVersion;
|
|
@@ -308,6 +378,23 @@ async function main() {
|
|
|
308
378
|
const titleAns = (await prompt(rl, ` title [${trim(idea.title, 60)}]: `)).trim();
|
|
309
379
|
const title = titleAns || idea.title;
|
|
310
380
|
|
|
381
|
+
// task 1003071 (R13): a triaged promotion NAMES its goal. The default
|
|
382
|
+
// comes from the stored hint, else the R07 suggester; '-' opts out
|
|
383
|
+
// explicitly, which falls through to the version's catch-all "general"
|
|
384
|
+
// bucket — now a choice someone typed rather than the silent default.
|
|
385
|
+
// The EFFECTIVE title, not idea.title: the suggester matches on title
|
|
386
|
+
// terms, and the operator may just have retitled this.
|
|
387
|
+
const gd = await resolveGoalDefault({ ...idea, title }, versionId);
|
|
388
|
+
if (gd.source === 'suggester') console.log(` suggested goal: ${gd.label}`);
|
|
389
|
+
if (gd.source === 'unavailable') console.log(' (goal suggester unreachable — enter a goal id, or "-" to accept the general bucket)');
|
|
390
|
+
const goalPrompt = gd.goalId != null
|
|
391
|
+
? ` goal [${gd.goalId}] ("-" for the version's general bucket): `
|
|
392
|
+
: ' goal (blank/"-" for the version\'s general bucket): ';
|
|
393
|
+
const goalAns = (await prompt(rl, goalPrompt)).trim();
|
|
394
|
+
const picked = applyGoalAnswer(gd.goalId, goalAns);
|
|
395
|
+
if (picked.warn) console.log(` ! ${picked.warn}`);
|
|
396
|
+
const goalId = picked.goalId;
|
|
397
|
+
|
|
311
398
|
try {
|
|
312
399
|
const task = await createTask({
|
|
313
400
|
versionId,
|
|
@@ -315,6 +402,7 @@ async function main() {
|
|
|
315
402
|
description: idea.body_md || '',
|
|
316
403
|
priority: Number.isFinite(priority) ? priority : null,
|
|
317
404
|
sourceRef: `idea:${idea.id}`,
|
|
405
|
+
goalId,
|
|
318
406
|
});
|
|
319
407
|
await patchIdea(idea.id, {
|
|
320
408
|
status: 'promoted',
|
|
@@ -323,7 +411,7 @@ async function main() {
|
|
|
323
411
|
});
|
|
324
412
|
summary.promoted++;
|
|
325
413
|
promotedIds.push({ ideaId: idea.id, taskId: task.id });
|
|
326
|
-
console.log(` ✓ promoted #${idea.id} → task #${task.id} (${versionId}, P${priority})`);
|
|
414
|
+
console.log(` ✓ promoted #${idea.id} → task #${task.id} (${versionId}, P${priority}${goalId != null ? `, goal ${goalId}` : ', → general bucket'})`);
|
|
327
415
|
} catch (err) {
|
|
328
416
|
console.error(` ! ${err.message}`);
|
|
329
417
|
}
|
|
@@ -360,6 +448,9 @@ if (require.main === module) {
|
|
|
360
448
|
}
|
|
361
449
|
|
|
362
450
|
module.exports = {
|
|
451
|
+
resolveGoalDefault,
|
|
452
|
+
applyGoalAnswer,
|
|
453
|
+
createTask,
|
|
363
454
|
resolveDevelopmentPath,
|
|
364
455
|
parseDevelopment,
|
|
365
456
|
loadDevelopment,
|
package/src/module-api.js
CHANGED
|
@@ -1005,8 +1005,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1005
1005
|
// landed since 1.19.287 with no explicit bump. run 32546561232. (task 1002620)
|
|
1006
1006
|
// 1.19.289 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1007
1007
|
// landed since 1.19.288 with no explicit bump. run 32549912116. (task 1002620)
|
|
1008
|
+
// 1.19.290 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1009
|
+
// landed since 1.19.289 with no explicit bump. run 32552538814. (task 1002620)
|
|
1008
1010
|
// ---------------------------------------------------------------------------
|
|
1009
|
-
const CORE_VERSION = '1.19.
|
|
1011
|
+
const CORE_VERSION = '1.19.290'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1010
1012
|
|
|
1011
1013
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1012
1014
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// tests/promote_goal_id.mjs — a triaged promotion lands IN A GOAL (task 1003071 / R13).
|
|
2
|
+
//
|
|
3
|
+
// Before this, both promote paths — the hall triage overlay and the /idea-triage
|
|
4
|
+
// CLI — created a task with no goal_id at all. That does NOT mean the task came
|
|
5
|
+
// out goal-less: db.createTask (modules/lifecycle/db.js) defaults a goal-less
|
|
6
|
+
// create into the version's catch-all "<version> — general" goal. That bucket IS
|
|
7
|
+
// the defect. It carries no done-when criteria, so since ADR 0183 made criteria
|
|
8
|
+
// close themselves off LINKED tasks, work parked there can never advance
|
|
9
|
+
// anything — and it does not read as real goal work on the board. Falling into it
|
|
10
|
+
// had to stop being the silent default.
|
|
11
|
+
//
|
|
12
|
+
// What these pin:
|
|
13
|
+
// 1. PRECEDENCE. The stored `suggested_goal_id` hint beats the R07 suggester.
|
|
14
|
+
// inbox.captureIdea writes that hint even when the matrix declined to route,
|
|
15
|
+
// specifically so triage "inherits the guess instead of re-deriving it"
|
|
16
|
+
// (core_222's own comment). A heuristic must never overrule a named goal.
|
|
17
|
+
// 2. OPT-OUT IS EXPLICIT. Taking the general bucket is still allowed — it just
|
|
18
|
+
// has to be chosen, not defaulted into.
|
|
19
|
+
// 3. DEGRADES, NEVER BLOCKS. A suggester outage must not stop a triage verdict.
|
|
20
|
+
// 4. BOTH PATHS send goal_id. Two surfaces, one contract — the same class of
|
|
21
|
+
// miss as the two claim-insert sites (task 1003137).
|
|
22
|
+
//
|
|
23
|
+
// Run: node tests/promote_goal_id.mjs
|
|
24
|
+
|
|
25
|
+
import { strict as assert } from 'node:assert';
|
|
26
|
+
import { createRequire } from 'node:module';
|
|
27
|
+
import { readFileSync } from 'node:fs';
|
|
28
|
+
import { makeRunner } from './helpers.mjs';
|
|
29
|
+
|
|
30
|
+
process.env.NODE_ENV = 'test';
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
const { test, summary } = makeRunner();
|
|
33
|
+
|
|
34
|
+
// triage.js captures `cliClient` by destructuring at require time, so a stub has
|
|
35
|
+
// to be in place BEFORE the module loads. Dropping both from the require cache
|
|
36
|
+
// and re-requiring is what makes that possible per-test.
|
|
37
|
+
function freshTriage() {
|
|
38
|
+
delete require.cache[require.resolve('../scripts/gds/triage.js')];
|
|
39
|
+
return require('../scripts/gds/triage.js');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const TRIAGE = readFileSync(new URL('../scripts/gds/triage.js', import.meta.url), 'utf8');
|
|
43
|
+
const HALL = readFileSync(new URL('../modules/hall-ui/public/task-detail.js', import.meta.url), 'utf8');
|
|
44
|
+
const SKILL = readFileSync(new URL('../.claude/skills/idea-triage/SKILL.md', import.meta.url), 'utf8');
|
|
45
|
+
|
|
46
|
+
// ==========================================================================
|
|
47
|
+
// BEHAVIOURAL — these INVOKE the code. The static assertions further down pin
|
|
48
|
+
// the wiring a unit test cannot reach (the DOM strings, the skill doc); they do
|
|
49
|
+
// not stand in for actually running it.
|
|
50
|
+
// ==========================================================================
|
|
51
|
+
|
|
52
|
+
const triage = require('../scripts/gds/triage.js');
|
|
53
|
+
|
|
54
|
+
// A stub of the generated typed client: records the call, answers from a script.
|
|
55
|
+
function stubClient(answer) {
|
|
56
|
+
const calls = [];
|
|
57
|
+
const client = async () => ({
|
|
58
|
+
goals: {
|
|
59
|
+
getGoalsSuggestions: async (req) => {
|
|
60
|
+
calls.push(req);
|
|
61
|
+
if (answer instanceof Error) throw answer;
|
|
62
|
+
return answer;
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
return { client, calls };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const SUGGESTED = {
|
|
70
|
+
ok: true,
|
|
71
|
+
data: { suggestions: [{ id: 1000071, title: 'Idea routing', score: 7, matched_terms: ['triage', 'goal'] }] },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
await test('BEHAVIOUR: a junk stored hint is ignored, not turned into goalId NaN', async () => {
|
|
75
|
+
for (const junk of ['not-an-id', '', '0', '-3', '4.5', {}]) {
|
|
76
|
+
const { client, calls } = stubClient(SUGGESTED);
|
|
77
|
+
const r = await triage.resolveGoalDefault({ suggested_goal_id: junk, title: 't' }, 'BV1', { client });
|
|
78
|
+
assert.notEqual(r.source, 'hint', `${JSON.stringify(junk)} is not a usable hint`);
|
|
79
|
+
assert.equal(calls.length, 1, 'and it falls through to the suggester rather than dead-ending');
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await test('BEHAVIOUR: a stored hint wins and the suggester is never even called', async () => {
|
|
84
|
+
const { client, calls } = stubClient(SUGGESTED);
|
|
85
|
+
const r = await triage.resolveGoalDefault({ suggested_goal_id: 1000058, title: 'x' }, 'BV1', { client });
|
|
86
|
+
assert.equal(r.goalId, 1000058);
|
|
87
|
+
assert.equal(r.source, 'hint');
|
|
88
|
+
assert.equal(calls.length, 0, 'a named goal must not be re-derived — that is the whole point of the column');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await test('BEHAVIOUR: with no hint, the top suggestion becomes the default, labelled with its score', async () => {
|
|
92
|
+
const { client, calls } = stubClient(SUGGESTED);
|
|
93
|
+
const r = await triage.resolveGoalDefault({ title: 'goal triage routing', body_md: 'b' }, 'BV1', { client });
|
|
94
|
+
assert.equal(r.goalId, 1000071);
|
|
95
|
+
assert.equal(r.source, 'suggester');
|
|
96
|
+
assert.match(r.label, /score 7/);
|
|
97
|
+
assert.match(r.label, /triage\/goal/, 'the matched terms are what let the operator judge it');
|
|
98
|
+
assert.equal(calls.length, 1);
|
|
99
|
+
assert.equal(calls[0].query.version, 'BV1', 'goals are per-version — the wrong version offers an invalid home');
|
|
100
|
+
assert.equal(calls[0].query.limit, 3);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await test('BEHAVIOUR: the body is capped before it goes on a query string', async () => {
|
|
104
|
+
const { client, calls } = stubClient(SUGGESTED);
|
|
105
|
+
await triage.resolveGoalDefault({ title: 't', body_md: 'x'.repeat(9000) }, 'BV1', { client });
|
|
106
|
+
assert.equal(calls[0].query.body.length, 2000, 'an un-capped body would blow the URL length');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await test('BEHAVIOUR: a suggester THROW degrades to no default, it does not propagate', async () => {
|
|
110
|
+
const { client } = stubClient(new Error('ECONNREFUSED'));
|
|
111
|
+
const r = await triage.resolveGoalDefault({ title: 't' }, 'BV1', { client });
|
|
112
|
+
assert.deepEqual(r, { goalId: null, label: null, source: 'unavailable' },
|
|
113
|
+
'a triage verdict must survive the suggester being down');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await test('BEHAVIOUR: a non-ok or empty response is "none", distinct from "unavailable"', async () => {
|
|
117
|
+
const empty = await triage.resolveGoalDefault({ title: 't' }, 'BV1',
|
|
118
|
+
{ client: stubClient({ ok: true, data: { suggestions: [] } }).client });
|
|
119
|
+
assert.equal(empty.source, 'none');
|
|
120
|
+
const notOk = await triage.resolveGoalDefault({ title: 't' }, 'BV1',
|
|
121
|
+
{ client: stubClient({ ok: false, status: 500, data: null }).client });
|
|
122
|
+
assert.equal(notOk.source, 'none', 'the two are reported differently because the operator advice differs');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await test('BEHAVIOUR: blank ACCEPTS the default; only "-"/"none" declines', () => {
|
|
126
|
+
assert.deepEqual(triage.applyGoalAnswer(1000071, ''), { goalId: 1000071, warn: null });
|
|
127
|
+
assert.deepEqual(triage.applyGoalAnswer(1000071, ' '), { goalId: 1000071, warn: null },
|
|
128
|
+
'whitespace is a bare Enter, not a decision');
|
|
129
|
+
assert.deepEqual(triage.applyGoalAnswer(1000071, '-'), { goalId: null, warn: null });
|
|
130
|
+
assert.deepEqual(triage.applyGoalAnswer(1000071, 'none'), { goalId: null, warn: null });
|
|
131
|
+
assert.deepEqual(triage.applyGoalAnswer(1000071, 'NONE'), { goalId: null, warn: null });
|
|
132
|
+
assert.deepEqual(triage.applyGoalAnswer(1000071, '1000058'), { goalId: 1000058, warn: null },
|
|
133
|
+
'an explicit id overrides the default');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await test('BEHAVIOUR: junk clears the goal and SAYS SO rather than landing on a wrong one', () => {
|
|
137
|
+
for (const junk of ['abc', '12.5', '-4', '0', 'goal 7']) {
|
|
138
|
+
const r = triage.applyGoalAnswer(1000071, junk);
|
|
139
|
+
assert.equal(r.goalId, null, `"${junk}" must not resolve to a goal`);
|
|
140
|
+
assert.match(r.warn, /general bucket/, `"${junk}" must say where it will land instead`);
|
|
141
|
+
}
|
|
142
|
+
// Number('') is 0 and Number(' ') is 0 — both would silently become goal 0
|
|
143
|
+
// without the blank check above running first. Guarding the pair together.
|
|
144
|
+
assert.equal(triage.applyGoalAnswer(9, '').goalId, 9);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await test('BEHAVIOUR: createTask puts goal_id on the body, and omits it entirely when null', async () => {
|
|
148
|
+
const seen = [];
|
|
149
|
+
const fakeClient = {
|
|
150
|
+
tasks: { postTasks: async (req) => { seen.push(req.body); return { ok: true, data: { task: { id: 7 } } }; } },
|
|
151
|
+
};
|
|
152
|
+
const cliLib = require('../scripts/gds/cli-lib.js');
|
|
153
|
+
const realCliClient = cliLib.cliClient;
|
|
154
|
+
cliLib.cliClient = async () => fakeClient;
|
|
155
|
+
try {
|
|
156
|
+
// triage.js destructured cliClient at require time, so patching the module
|
|
157
|
+
// object cannot reach it — re-require in a fresh registry instead.
|
|
158
|
+
const fresh = freshTriage();
|
|
159
|
+
await fresh.createTask({ versionId: 'BV1', title: 't', description: 'd', priority: 3, sourceRef: 'idea:1', goalId: 1000071 });
|
|
160
|
+
await fresh.createTask({ versionId: 'BV1', title: 't', description: 'd', priority: 3, sourceRef: 'idea:2' });
|
|
161
|
+
assert.equal(seen[0].goal_id, 1000071, 'the goal must reach the server');
|
|
162
|
+
assert.ok(!('goal_id' in seen[1]),
|
|
163
|
+
'and opting out must send NO goal_id key — the server\'s catch-all only applies on absent');
|
|
164
|
+
assert.equal(seen[0].source, 'idea_inbox', 'provenance still travels');
|
|
165
|
+
assert.equal(seen[0].status, 'backlog');
|
|
166
|
+
} finally {
|
|
167
|
+
cliLib.cliClient = realCliClient;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await test('BEHAVIOUR: a failed create throws with the status, it does not return a phantom task', async () => {
|
|
172
|
+
const cliLib = require('../scripts/gds/cli-lib.js');
|
|
173
|
+
const realCliClient = cliLib.cliClient;
|
|
174
|
+
cliLib.cliClient = async () => ({
|
|
175
|
+
tasks: { postTasks: async () => ({ ok: false, status: 422, data: { error: 'NO_SUCH_GOAL' } }) },
|
|
176
|
+
});
|
|
177
|
+
try {
|
|
178
|
+
const fresh = freshTriage();
|
|
179
|
+
await assert.rejects(
|
|
180
|
+
() => fresh.createTask({ versionId: 'BV1', title: 't', description: 'd', goalId: 999999999 }),
|
|
181
|
+
/422/,
|
|
182
|
+
);
|
|
183
|
+
} finally {
|
|
184
|
+
cliLib.cliClient = realCliClient;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ==========================================================================
|
|
189
|
+
// STATIC — the surfaces a unit test cannot invoke (browser DOM strings, docs).
|
|
190
|
+
// ==========================================================================
|
|
191
|
+
|
|
192
|
+
// ---- 4. both paths send goal_id -------------------------------------------
|
|
193
|
+
|
|
194
|
+
await test('BOTH PATHS: the CLI createTask and the hall payload each send goal_id', () => {
|
|
195
|
+
assert.match(TRIAGE, /if \(goalId != null\) body\.goal_id = Number\(goalId\);/,
|
|
196
|
+
'CLI: goal_id must reach the create body');
|
|
197
|
+
assert.match(TRIAGE, /async function createTask\(\{[^}]*goalId/,
|
|
198
|
+
'CLI: createTask must accept it rather than ignore the caller');
|
|
199
|
+
assert.match(HALL, /payload\.goal_id = Number\(form\.goal\.value\)/,
|
|
200
|
+
'hall: goal_id must reach the create payload');
|
|
201
|
+
assert.match(HALL, /data-goal-select/, 'hall: and there must be a picker to choose it from');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await test('BOTH PATHS: the stale "does not accept kind" note is corrected, not left to mislead', () => {
|
|
205
|
+
assert.ok(!/doesn't currently accept 'kind'/.test(TRIAGE),
|
|
206
|
+
'that claim was false — routes/tasks.js takes kind and the hall path sends it');
|
|
207
|
+
assert.match(TRIAGE, /no longer true/, 'and the correction says so explicitly');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ---- 1. precedence: the stored hint wins ---------------------------------
|
|
211
|
+
|
|
212
|
+
// (The CLI's precedence is proven by the BEHAVIOUR block above — it drives the
|
|
213
|
+
// real function and asserts the suggester is never called on a hint. A source-shape
|
|
214
|
+
// assertion here would only restate that, less reliably.)
|
|
215
|
+
|
|
216
|
+
await test('PRECEDENCE: the hall picker marks the hint selected, and only auto-selects a suggestion without one', () => {
|
|
217
|
+
const fn = HALL.slice(HALL.indexOf('async function goalOptionsFor'), HALL.indexOf('async function renderPromote'));
|
|
218
|
+
assert.match(fn, /idea\.suggested_goal_id/, 'the hint is read');
|
|
219
|
+
assert.match(fn, /carried from the filing/, 'and labelled as inherited, not guessed');
|
|
220
|
+
assert.match(fn, /hint == null && i === 0 \? ' selected' : ''/,
|
|
221
|
+
'a suggestion is pre-selected ONLY when there is no hint to honour');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ---- the suggester's weak-evidence signal is shown, not hidden -----------
|
|
225
|
+
|
|
226
|
+
await test('the suggester default shows score + matched terms so it can be judged', () => {
|
|
227
|
+
assert.match(TRIAGE, /score \$\{top\.score\}/, 'CLI shows the score');
|
|
228
|
+
assert.match(TRIAGE, /matched_terms/, 'CLI shows why it matched');
|
|
229
|
+
assert.match(HALL, /matched: /, 'hall shows why it matched');
|
|
230
|
+
// task 1003062 documented that token overlap can match an incidental word; the
|
|
231
|
+
// visible score is what stops that being an invisible wrong default.
|
|
232
|
+
assert.match(HALL, /incidental word/, 'and the reason it is shown is written down');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// ---- 2. opting out is explicit ------------------------------------------
|
|
236
|
+
|
|
237
|
+
await test('OPT-OUT: the prompt itself offers the way out, and the branch delegates the meaning', () => {
|
|
238
|
+
const branch = TRIAGE.slice(TRIAGE.indexOf('const gd = await resolveGoalDefault'), TRIAGE.indexOf('const task = await createTask'));
|
|
239
|
+
assert.match(branch, /general bucket/, 'the prompt has to say where opting out lands');
|
|
240
|
+
assert.match(branch, /applyGoalAnswer\(gd\.goalId, goalAns\)/,
|
|
241
|
+
'the branch must delegate to the tested helper, not re-implement the parsing inline');
|
|
242
|
+
assert.match(branch, /picked\.warn/, 'and surface its warning');
|
|
243
|
+
assert.match(branch, /resolveGoalDefault\(\{ \.\.\.idea, title \}, versionId\)/,
|
|
244
|
+
'the suggester must see the title the operator just confirmed, not the filed one');
|
|
245
|
+
// The semantics themselves are covered by the BEHAVIOUR block above — this
|
|
246
|
+
// only pins that main() is actually wired to them.
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await test('OPT-OUT: the hall names where opting out actually lands, and only defaults to it last', () => {
|
|
250
|
+
const fn = HALL.slice(HALL.indexOf('async function goalOptionsFor'), HALL.indexOf('async function renderPromote'));
|
|
251
|
+
// "— no goal —" would be a lie: the server files a goal_id-less create under
|
|
252
|
+
// the version's catch-all. The label has to say so.
|
|
253
|
+
assert.match(fn, /general bucket/);
|
|
254
|
+
assert.ok(!/— no goal —/.test(fn), 'the honest label replaced the misleading one');
|
|
255
|
+
assert.match(fn, /opts\.length === 0 \? ' selected' : ''/, 'the opt-out is the fallback, not the default');
|
|
256
|
+
assert.match(fn, /a choice rather than the silent default/, 'and the intent is stated');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await test('the CLI result line names the goal, or says the task fell into the general bucket', () => {
|
|
260
|
+
assert.match(TRIAGE, /goal \$\{goalId\}` : ', → general bucket'/,
|
|
261
|
+
'where it landed must be visible in the output, both ways');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// ---- 3. degrades, never blocks -----------------------------------------
|
|
265
|
+
|
|
266
|
+
await test('DEGRADES: a suggester outage yields no default rather than throwing', () => {
|
|
267
|
+
const fn = TRIAGE.slice(TRIAGE.indexOf('async function resolveGoalDefault'), TRIAGE.indexOf('async function createTask'));
|
|
268
|
+
assert.match(fn, /catch \(_\) \{\s*return \{ goalId: null, label: null, source: 'unavailable' \}/,
|
|
269
|
+
'the CLI must swallow it and report unavailable');
|
|
270
|
+
assert.match(TRIAGE, /goal suggester unreachable/, 'and tell the operator what happened');
|
|
271
|
+
const hallFn = HALL.slice(HALL.indexOf('async function goalOptionsFor'), HALL.indexOf('async function renderPromote'));
|
|
272
|
+
assert.match(hallFn, /catch \(_\) \{ suggestions = \[\]; \}/, 'the hall must degrade to no suggestions');
|
|
273
|
+
assert.match(hallFn, /must not stop a triage verdict/, 'the reasoning is recorded');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await test('the hall guards the stored hint against a junk value reaching the DOM', () => {
|
|
277
|
+
const fn = HALL.slice(HALL.indexOf('async function goalOptionsFor'), HALL.indexOf('async function renderPromote'));
|
|
278
|
+
assert.match(fn, /Number\.isFinite\(raw\) && raw > 0 \? raw : null/,
|
|
279
|
+
'value="NaN" would be POSTed as a goal id');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
await test('the hall degrades a DEAD SESSION to no suggestions instead of navigating away', () => {
|
|
283
|
+
const fn = HALL.slice(HALL.indexOf('async function goalOptionsFor'), HALL.indexOf('async function renderPromote'));
|
|
284
|
+
// getJSON without softAuth calls location.assign on a 401 — from a background
|
|
285
|
+
// fetch that would discard whatever the builder had typed into the form.
|
|
286
|
+
assert.match(fn, /softAuth: true/);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
await test('DEGRADES: the hall re-fetches goals when the version changes, newest-wins', () => {
|
|
290
|
+
// Two quick version changes race; an out-of-order resolve would paint another
|
|
291
|
+
// version's goals, all of which the server would reject.
|
|
292
|
+
assert.match(HALL, /var mine = \+\+goalFetch;/);
|
|
293
|
+
assert.match(HALL, /if \(mine !== goalFetch\) return;/);
|
|
294
|
+
// Goals belong to a version; keeping stale options would offer a home the
|
|
295
|
+
// server would reject.
|
|
296
|
+
assert.match(HALL, /vsel\.addEventListener\('change'/);
|
|
297
|
+
assert.match(HALL, /await goalOptionsFor\(vsel\.value\)/);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
await test('the CLI uses the GENERATED typed client method, not the raw escape hatch', () => {
|
|
301
|
+
assert.match(TRIAGE, /api\.goals\.getGoalsSuggestions\(/,
|
|
302
|
+
'so a path or param rename reds the client build instead of failing at runtime');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// ---- the skill contract ------------------------------------------------
|
|
306
|
+
|
|
307
|
+
await test('SKILL DOC: the inbox is declared homeless-only, with both consequences spelled out', () => {
|
|
308
|
+
assert.match(SKILL, /HOMELESS-ONLY/, 'the contract is stated');
|
|
309
|
+
assert.match(SKILL, /Every promote names a goal/, 'consequence 1');
|
|
310
|
+
assert.match(SKILL, /general` goal/, 'and it is honest about what opting out means');
|
|
311
|
+
assert.match(SKILL, /inherited, not guessed/, 'consequence 2 — precedence');
|
|
312
|
+
assert.match(SKILL, /0183/, 'and why a goal-less task now matters more than it used to');
|
|
313
|
+
assert.match(SKILL, /HOMELESS/i, 'the description line carries it too');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
summary();
|