@bongos/core 1.19.500 → 1.19.502
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 +22 -17
- package/.gitattributes +10 -0
- package/docs/adr/0082-server-side-merge-conflict-auto-resolution.md +4 -0
- package/docs/adr/0234-idea-routing-capture-time-promotion-landing-matrix-homeless-inbox.md +9 -7
- package/docs/adr/0235-a-fix-category-is-kindbug-under-a-goal-no-column-required.md +131 -0
- package/docs/adr/README.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/git-merge-regen.js +20 -0
- package/src/module-api.js +1 -1
- package/tests/git_merge_regen.mjs +87 -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.502",
|
|
6
|
+
"core_contract": "1.19.502",
|
|
7
|
+
"source_commit": "0028d2b0b8159fb3da54835fd45eeb3ae98f7916",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-03T16:16:35.542Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 426,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
14
|
"functional_verbatim": 1942,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2392,
|
|
20
|
+
"tree_sha256": "ad1558679b2605e11cf647772ee24f67c5bd59c0e1d98bcdf81d7a795c456654",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -317,7 +317,7 @@
|
|
|
317
317
|
{
|
|
318
318
|
"path": ".gitattributes",
|
|
319
319
|
"mode": "0000644",
|
|
320
|
-
"sha256": "
|
|
320
|
+
"sha256": "3848ed851a03fd11f8dae94c1e94b802c4aa625efaa7f4aff27438a1fdfb67ac"
|
|
321
321
|
},
|
|
322
322
|
{
|
|
323
323
|
"path": "CLAUDE.md",
|
|
@@ -877,7 +877,7 @@
|
|
|
877
877
|
{
|
|
878
878
|
"path": "docs/adr/0082-server-side-merge-conflict-auto-resolution.md",
|
|
879
879
|
"mode": "0000644",
|
|
880
|
-
"sha256": "
|
|
880
|
+
"sha256": "7da7140202139c123e832f527167c96de18eb498ae21243cd30cea6b133c8f90"
|
|
881
881
|
},
|
|
882
882
|
{
|
|
883
883
|
"path": "docs/adr/0083-modular-architecture-strangler-build-plan.md",
|
|
@@ -1677,12 +1677,17 @@
|
|
|
1677
1677
|
{
|
|
1678
1678
|
"path": "docs/adr/0234-idea-routing-capture-time-promotion-landing-matrix-homeless-inbox.md",
|
|
1679
1679
|
"mode": "0000644",
|
|
1680
|
-
"sha256": "
|
|
1680
|
+
"sha256": "417d1faffdc644b6dbad3d27fc58fac7d87609550060d0cd4edb0bad702d6b4c"
|
|
1681
|
+
},
|
|
1682
|
+
{
|
|
1683
|
+
"path": "docs/adr/0235-a-fix-category-is-kindbug-under-a-goal-no-column-required.md",
|
|
1684
|
+
"mode": "0000644",
|
|
1685
|
+
"sha256": "f3573d82c77dc2b7d006f4ee8051b9b5a247669d2ac4c046c2de7c282d4e65ff"
|
|
1681
1686
|
},
|
|
1682
1687
|
{
|
|
1683
1688
|
"path": "docs/adr/README.md",
|
|
1684
1689
|
"mode": "0000644",
|
|
1685
|
-
"sha256": "
|
|
1690
|
+
"sha256": "43d782f6ca3f187f280e13466830ceceaf6ba74da2cc669e8b7dd6ece166d657"
|
|
1686
1691
|
},
|
|
1687
1692
|
{
|
|
1688
1693
|
"path": "docs/api-reference.md",
|
|
@@ -2567,7 +2572,7 @@
|
|
|
2567
2572
|
{
|
|
2568
2573
|
"path": "docs/module-api-changelog.md",
|
|
2569
2574
|
"mode": "0000644",
|
|
2570
|
-
"sha256": "
|
|
2575
|
+
"sha256": "bccd3e96df6a344e806052af69493507b4ccab78ab3149aa10f7373d8546fb97"
|
|
2571
2576
|
},
|
|
2572
2577
|
{
|
|
2573
2578
|
"path": "docs/modules-contract.md",
|
|
@@ -7212,12 +7217,12 @@
|
|
|
7212
7217
|
{
|
|
7213
7218
|
"path": "package-lock.json",
|
|
7214
7219
|
"mode": "0000644",
|
|
7215
|
-
"sha256": "
|
|
7220
|
+
"sha256": "5911d20e472c017d950ca2154d1c0e86e7479e07c9efb4a02eecea0534aae58c"
|
|
7216
7221
|
},
|
|
7217
7222
|
{
|
|
7218
7223
|
"path": "package.json",
|
|
7219
7224
|
"mode": "0000644",
|
|
7220
|
-
"sha256": "
|
|
7225
|
+
"sha256": "2220826fc5bb64636838a79c72229e973109a28639174748ca7e2fb505192f97"
|
|
7221
7226
|
},
|
|
7222
7227
|
{
|
|
7223
7228
|
"path": "public-docs/index.html",
|
|
@@ -7732,7 +7737,7 @@
|
|
|
7732
7737
|
{
|
|
7733
7738
|
"path": "scripts/gds/git-merge-regen.js",
|
|
7734
7739
|
"mode": "0000644",
|
|
7735
|
-
"sha256": "
|
|
7740
|
+
"sha256": "f9a391ac6f32b2d5ca939b48186b6d9cb7aa171554652a5b5afbc4a408aae203"
|
|
7736
7741
|
},
|
|
7737
7742
|
{
|
|
7738
7743
|
"path": "scripts/gds/go-live.js",
|
|
@@ -8887,7 +8892,7 @@
|
|
|
8887
8892
|
{
|
|
8888
8893
|
"path": "src/module-api.js",
|
|
8889
8894
|
"mode": "0000644",
|
|
8890
|
-
"sha256": "
|
|
8895
|
+
"sha256": "636b358e8be667bd38743b42009ee1101b0c330584fc647f71424c03fe166c71"
|
|
8891
8896
|
},
|
|
8892
8897
|
{
|
|
8893
8898
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9877,7 +9882,7 @@
|
|
|
9877
9882
|
{
|
|
9878
9883
|
"path": "tests/git_merge_regen.mjs",
|
|
9879
9884
|
"mode": "0000644",
|
|
9880
|
-
"sha256": "
|
|
9885
|
+
"sha256": "2d3574914c1f9c843396f498a8e68c2fd8ec2a5bc574c98f64dc2505aa1ccba8"
|
|
9881
9886
|
},
|
|
9882
9887
|
{
|
|
9883
9888
|
"path": "tests/github_push_ancestry.mjs",
|
package/.gitattributes
CHANGED
|
@@ -74,6 +74,16 @@ src/bongos/CLAUDE.md merge=otb-regen
|
|
|
74
74
|
src/bongos/routes/CLAUDE.md merge=otb-regen
|
|
75
75
|
scripts/gds/CLAUDE.md merge=otb-regen
|
|
76
76
|
|
|
77
|
+
# docs/copy-registry.json + docs/copy-inventory.md (task 1003543): the copy
|
|
78
|
+
# registry's committed pair (scripts/gds/copy-inventory.js) — fully generated from
|
|
79
|
+
# a tree scan, nothing hand-authored in either, so two branches that each touch
|
|
80
|
+
# hall/status/landing copy regenerate them differently and conflict like ordinary
|
|
81
|
+
# files. Same otb-regen shape as docs/session-log-index.md above (whole-file
|
|
82
|
+
# rebuild, all three merge sides ignored); REGENERABLE in git-merge-regen.js
|
|
83
|
+
# carries the matching rule.
|
|
84
|
+
docs/copy-registry.json merge=otb-regen
|
|
85
|
+
docs/copy-inventory.md merge=otb-regen
|
|
86
|
+
|
|
77
87
|
# Append-only index tables (task 1385 / ADR 0082). docs/adr/README.md is a
|
|
78
88
|
# hand-maintained ADR index whose ONLY recurring conflict is two branches each
|
|
79
89
|
# appending a new row — there is no generator to regen it from, so `otb-regen`
|
|
@@ -48,6 +48,10 @@ The PR's required checks (unit / trufflehog / audit / gate-review) re-run on the
|
|
|
48
48
|
- `docs/adr/README.md` itself now carries `merge=union`, so this ADR's own index row — and every future one — stops conflicting.
|
|
49
49
|
- The generated-artifact set is defined once (`REGENERABLE` in `git-merge-regen.js`, mirrored by `.gitattributes` and the `fitness.js` freshness gate); adding a future generated file means adding it in those places together.
|
|
50
50
|
|
|
51
|
+
## Addendum (task 1003543, 2026-09-03) — the copy registry joined the whole-file set
|
|
52
|
+
|
|
53
|
+
Two hall PRs in one session (526 for task 1003069, 531 for task 1003157) both ran `scripts/gds/copy-inventory.js` and sat `confirmed` with GitHub `mergeable=CONFLICTING` for 20+ minutes: `docs/copy-inventory.md` + `docs/copy-registry.json` carried no `.gitattributes` entry and no `REGENERABLE` rule, so they conflicted as ordinary text/JSON and the server resolver could not heal them (`resolveBranchConflicts` *did* run — its log line fired for both PRs — but the `otb-regen` driver refused the unregistered paths and the merge came back non-zero, which the resolver correctly reported as `unresolvable_conflict`; the reconciler dispatch was never the gap). Both files are, like `docs/repo-map.md` and `docs/session-log-index.md`, 100% generated from a tree scan with nothing hand-authored in either — `copy-inventory.js`'s own header says so ("Deterministic on a given tree — regenerate and commit rather than hand-editing"), so they needed the whole-file `otb-regen` shape, not `union`. The fix: `docs/copy-registry.json merge=otb-regen` + `docs/copy-inventory.md merge=otb-regen` in `.gitattributes`, both added to `REGENERABLE` in `git-merge-regen.js` with a regeneration rule that calls `copy-inventory.js`'s `buildRegistry()`/`renderReport()` directly (ignoring all three merge sides, same as the repo-map heal). No change was needed to `conflict-resolve.js`, `publish-reconciler.js`, or `ship-preflight-steps.js` — none of the three keeps its own copy of the generated-file list; all three read `REGENERABLE` (or, for the resolver, register the driver that reads it), so the new pair is covered everywhere the existing four already were.
|
|
54
|
+
|
|
51
55
|
## Alternatives considered
|
|
52
56
|
|
|
53
57
|
- **Lower deploy/merge access to `thetes`** (the owner's first instinct) — rejected: reopens the Builder 25 hole for zero benefit here (the stranded session was already Archon; rank was never the blocker).
|
|
@@ -155,13 +155,15 @@ as `#ideas` has always given that person, which is the actual "nothing lands unv
|
|
|
155
155
|
survived.
|
|
156
156
|
|
|
157
157
|
The companion interview decision — **"Discord `#ideas` files with HINTS only (`suggested_goal_id`),
|
|
158
|
-
never routes"** — is
|
|
159
|
-
|
|
160
|
-
`modules/discord/discord-inbound.js`'s `#ideas` capture
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
158
|
+
never routes"** — is what R12 (task 1003068, "Discord `#ideas` carries routing hints") shipped, correcting
|
|
159
|
+
this ADR's earlier divergence note (which read `status='ready'` at the time it was written).
|
|
160
|
+
`modules/discord/discord-inbound.js`'s `#ideas` capture now sets `suggested_goal_id` (a classifier guess
|
|
161
|
+
via `suggestGoalForCapture`) and `suggested_version` (defaulting to the most-recently-started open
|
|
162
|
+
version even when no goal matches) on the filed idea row — both READ-ONLY hints for a human triaging via
|
|
163
|
+
`/idea-triage` (R13) — and never sets `goalId`, the only field that would make `captureIdea` actually
|
|
164
|
+
route. Every `#ideas` filing still lands in `idea_inbox` exactly as the pre-routing path did, now
|
|
165
|
+
carrying a hint rather than none, consistent with every other "classifier guess, not a route" vector
|
|
166
|
+
this ADR documents — the second bypass idea 1000771 named did not reopen.
|
|
165
167
|
|
|
166
168
|
## Consequences
|
|
167
169
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# 0235 — A fix category is `kind='bug'` under a goal, not a new column
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-09-03
|
|
5
|
+
- **Tasks:** [#1003215](https://cloudbongos.com/builders#/task/1003215) (this decision — the structural half
|
|
6
|
+
of idea 1000828), [#1003214](https://cloudbongos.com/builders#/task/1003214) (the advisory's first quiet
|
|
7
|
+
case), [#1003157](https://cloudbongos.com/builders#/task/1003157) (extended the same exemption to the
|
|
8
|
+
criterion-link advisory and the goal detail view)
|
|
9
|
+
- **Goal:** [#1000071](https://cloudbongos.com/builders#/goal/1000071) — Idea routing
|
|
10
|
+
- **Related:** [ADR 0086](<redacted>.md) (the real work hierarchy this decision keeps
|
|
11
|
+
intact), [ADR 0183](<redacted>.md) (why a container that never finishes is dangerous
|
|
12
|
+
now), [ADR 0192 "a category orients and authorises nothing"](<redacted>.md)
|
|
13
|
+
(the precedent this decision is *not* — see §2)
|
|
14
|
+
|
|
15
|
+
## 1. The problem
|
|
16
|
+
|
|
17
|
+
A fix for something that should already work needs a home. Filing it with no
|
|
18
|
+
`goal_id` lands it in the version's catch-all — no scope wall, no owner reviewing
|
|
19
|
+
it (the `no_goal`/`catch_all` gap `goal-advisory.js` already flags for everything
|
|
20
|
+
else). Filing it under a real goal used to trip a second, unrelated nudge: every
|
|
21
|
+
goal-bound task with no done-when criterion link reads as unattributed work
|
|
22
|
+
(task 1003157). Neither nudge is wrong for a *feature* — but a bug fix is not
|
|
23
|
+
delivering toward one of a goal's stated outcomes; it is restoring one. Idea
|
|
24
|
+
1000828 named this a missing **first-class grouping**: "a fix for something that
|
|
25
|
+
should already work," living *under* an existing goal, without a new goal being
|
|
26
|
+
born for it.
|
|
27
|
+
|
|
28
|
+
## 2. Why this is not a stored category, and not [ADR 0192](<redacted>.md)
|
|
29
|
+
|
|
30
|
+
The obvious-looking answer — add a `category` (or `bucket`) column to `tasks` —
|
|
31
|
+
is the wrong shape for two reasons specific to *this* system's schema, not taste:
|
|
32
|
+
|
|
33
|
+
1. **`tasks` stores no category at all, by decision.** [ADR 0192 "a category
|
|
34
|
+
orients and authorises nothing"](<redacted>.md)
|
|
35
|
+
already answered a structurally identical question for *goals* — a human label
|
|
36
|
+
from an owner-named set, advisory only, gating nothing — and its own text is
|
|
37
|
+
explicit that "a task stores no category… it inherits its goal's through
|
|
38
|
+
`tasks.goal_id`; a stored copy would be a second answer to one question." A
|
|
39
|
+
`tasks.fix_category` boolean would be exactly that second answer: `kind='bug'`
|
|
40
|
+
already exists, is already required (`migrations/006_pms_v2.sql`,
|
|
41
|
+
`NOT NULL DEFAULT 'unclassified'`), and already means "this is a fix." Adding
|
|
42
|
+
a column to restate it is duplication, not a new fact.
|
|
43
|
+
2. **This decision is a *narrower*, more binding one than [ADR 0192](<redacted>.md)'s.**
|
|
44
|
+
Its category is purely a label a human reads — nothing may branch on it.
|
|
45
|
+
"A bug fix needs no criterion link" and "a bug fix in the catch-all is quiet"
|
|
46
|
+
are the OPPOSITE: two decision surfaces (`goal-advisory.js`) DO branch on
|
|
47
|
+
`kind='bug'`, on purpose, because the alternative — nagging every bug fix to
|
|
48
|
+
either name a criterion it doesn't serve or justify a goal it doesn't need —
|
|
49
|
+
is the noise idea 1000828 asked to remove. So this ADR does not extend ADR
|
|
50
|
+
0192's "orients, never authorises" rule to `kind`; `kind` already carries real
|
|
51
|
+
weight elsewhere in this codebase (`task-classifier.js`'s heuristic
|
|
52
|
+
classifier decides it from title/description, and `<redacted>`
|
|
53
|
+
calibrates a task's estimate by it), and this decision adds one more narrow,
|
|
54
|
+
named case to that existing list rather than inventing a second category axis
|
|
55
|
+
beside it.
|
|
56
|
+
|
|
57
|
+
## 3. Why not a global "Bugs" goal either
|
|
58
|
+
|
|
59
|
+
Idea 1000828's own reasoning is the load-bearing part, and it survives unchanged
|
|
60
|
+
here: a goal carries `scope_modules`, the [ADR 0086](<redacted>.md)
|
|
61
|
+
§3 scope wall — a bugs goal spanning every module makes that wall either
|
|
62
|
+
everything or nothing, which is not a wall. And a goal carries done-when criteria
|
|
63
|
+
and can be marked `achieved`; a bug container never finishes, so making it a goal
|
|
64
|
+
manufactures a goal that can never legitimately reach `achieved` — polluting the
|
|
65
|
+
exact signal [ADR 0183](<redacted>.md)'s auto-satisfy now
|
|
66
|
+
depends on to cascade a goal to `achieved` the moment every linked task ships. A
|
|
67
|
+
category *under* an existing goal keeps that goal's wall, keeps the "things that
|
|
68
|
+
should work in THIS area" context, and never claims to be finishable on its own.
|
|
69
|
+
|
|
70
|
+
## 4. Decision
|
|
71
|
+
|
|
72
|
+
**A fix category is `kind='bug'` on a task that carries a real `goal_id` — no
|
|
73
|
+
schema change, no new column, no new goal type.** The goal is *finishable* as
|
|
74
|
+
before; the fix category is a lens over its existing `kind` field, exactly the
|
|
75
|
+
way [ADR 0192](<redacted>.md)'s goal category
|
|
76
|
+
is a lens over `goals.category_id`, just enforced more strictly because two real
|
|
77
|
+
gates read it.
|
|
78
|
+
|
|
79
|
+
**As-built** (task 1003214 first, task 1003157 completing it — both already
|
|
80
|
+
shipped on `main` before this ADR was written):
|
|
81
|
+
|
|
82
|
+
- `adviseGoal()` in [`modules/lifecycle/goal-advisory.js`](../../modules/lifecycle/goal-advisory.js)
|
|
83
|
+
is the single place all three exemptions live, in one pure, DB-free function:
|
|
84
|
+
a `kind='bug'` task is silent for `no_goal` (no goal at all — a fix needs no
|
|
85
|
+
home to be filed correctly), `catch_all` (a fix sitting in the version's
|
|
86
|
+
catch-all bucket is already where it belongs), and `no_criterion_link` (a real
|
|
87
|
+
goal's criteria named, but a bug fix delivers no criterion and needs none). The
|
|
88
|
+
function's own doc comment states the rule this ADR records: *"a fix is the
|
|
89
|
+
goal's 'fix category', not a delivery against one of its stated outcomes."*
|
|
90
|
+
- `countTasksWithoutCriterionLink()` in
|
|
91
|
+
[`modules/lifecycle/db-deps-criteria.js`](../../modules/lifecycle/db-deps-criteria.js),
|
|
92
|
+
surfaced on `GET /goals/:id` and rendered on the hall's goal detail view
|
|
93
|
+
(`modules/hall-ui/public/goals-render.js`), excludes `kind='bug'` from its
|
|
94
|
+
count for the identical reason — an intentionally unlinked fix must not read as
|
|
95
|
+
a standing defect on the goal's own page.
|
|
96
|
+
- **No hall/status change was needed for grouping.** Verified against the live
|
|
97
|
+
code, not assumed: every task card in a goal's To-do list already renders its
|
|
98
|
+
`kind` as a visible tag (`goals-render.js`, `task__tag`), and the goal detail
|
|
99
|
+
view's Filters disclosure already offers a `Kind` lens with `bug` as a value
|
|
100
|
+
(`goals-page.js`, `f-goal-kind`) — so "the hall groups bugs by kind" was
|
|
101
|
+
already true before this task, via the *same* general-purpose kind facility
|
|
102
|
+
every other kind uses. Building a bug-specific grouping widget beside it would
|
|
103
|
+
be the second-hierarchy mistake [ADR 0192](<redacted>.md)
|
|
104
|
+
§2 already named once for goal categories.
|
|
105
|
+
|
|
106
|
+
## 5. What was deliberately not built
|
|
107
|
+
|
|
108
|
+
- **No `tasks.category` / `tasks.fix_category` column, no enum, no CHECK
|
|
109
|
+
constraint.** `kind` already answers the question.
|
|
110
|
+
- **No global "Bugs" goal**, and no goal-creation shortcut that manufactures one
|
|
111
|
+
— §3 above is why that door stays shut.
|
|
112
|
+
- **No bug-specific grouping UI.** The kind tag + Kind filter already exist and
|
|
113
|
+
already work for `bug` the same as every other kind; a second, bug-only lens
|
|
114
|
+
beside a general one would duplicate it for no reader benefit.
|
|
115
|
+
|
|
116
|
+
## 6. Consequences
|
|
117
|
+
|
|
118
|
+
- Idea 1000820 and idea 1000788 (the `node_modules/pino` worktree-resolution
|
|
119
|
+
bugs cited as this decision's first real customers) now have somewhere to file
|
|
120
|
+
that advances a goal without inventing one — the mechanism task 1003215 asked
|
|
121
|
+
for was already load-bearing on `main` by the time this ADR was written; this
|
|
122
|
+
document is what makes the decision citable rather than three commit messages.
|
|
123
|
+
- The exemption is enforced in exactly one function (`adviseGoal`) and one query
|
|
124
|
+
(`countTasksWithoutCriterionLink`); a third surface that starts branching on
|
|
125
|
+
`kind='bug'` for a similar reason should extend those, not invent a fourth
|
|
126
|
+
definition of "what counts as a fix."
|
|
127
|
+
- If a future need ever wants `kind='bug'` to carry MORE weight than an advisory
|
|
128
|
+
exemption — a claim-time gate, a reward multiplier, a dependency rule — that is
|
|
129
|
+
a new, explicit decision needing its own ADR, the same posture
|
|
130
|
+
[ADR 0192](<redacted>.md) §5 holds for goal
|
|
131
|
+
categories. This ADR does not pre-authorise that.
|
package/docs/adr/README.md
CHANGED
|
@@ -322,5 +322,6 @@ This keeps the decision history honest and traceable.
|
|
|
322
322
|
|
|
323
323
|
| 0233 | [**A copy proposal is a task carrying a patch, and the applier is the only writer** ([task 1003114](https://cloudbongos.com/builders#/task/1003114) · goal 1000074, criterion `artist-edits-land-as-tasks`). Goal 1000074 asks for two things that pull against each other in its own text: the artist must change copy WITHOUT an engineer, and there must be NO LIVE CMS because every copy change is a claimed, graded, shipped task. R02 shipped the half that needs no reconciliation (the flag) and [ADR 0178](<redacted>.md) §3 pinned the absence of any column that could hold replacement text, naming this task as the one that would have to keep it true. Decisions: (1) the shape is **borrowed from the design half, not invented** — ADR 0081 already has a tool authoring and an `adapter.export()` landing the output as a committed diff under a claim inside a path guard, so the artist is the tool, the copy desk is where they author, and the proposal is the export. (2) A proposal is a **TASK carrying a fenced `copy-proposal` patch**, never a row: the ledger is where a proposal belongs anyway, nothing renders a task description as product copy, a reviewer can read the change without running anything, and `copy-desk` gains NO migration (the obvious-but-wrong R03 is a `copy_desk_proposals` table, which is the forbidden column with extra steps). The module gains `consumes: ["lifecycle"]` and calls `createTask` through the port — it can put a request INTO the ledger and nothing can pull copy OUT of it. (3) `scripts/gds/copy-apply.js` **re-derives the position** rather than trusting the patch, which required teaching R01s extractor to record a source SPAN and deliberately dropping it from the committed registry: an offset goes stale the moment anything above it changes, while the id is designed to survive exactly that movement, so the span is recomputed at the moment of the edit by the same code that decided the string was copy. (4) The **placeholder arithmetic** is what makes a patch mechanical rather than a note for an engineer — the applier puts the sources own `${expr}` back, k-th hole to k-th expression, so an artist may MOVE a value within a sentence but never add or drop one, checked at proposal time and again against the real source. (5) Every ambiguity is a **named refusal** (`target_gone`, `ambiguous_target`, `no_exact_span`, `placeholder_mismatch`, `path_outside_surface` — the design adapters path guard, derived from R01s own SURFACES), because a wrong edit is worse than no edit; after writing, the applier regenerates the registry and checks that what it wrote is what the inventory now reads. (6) Proposing is **open to every rank** (an artist is a craft, not a rank) and lands at `backlog`, so a proposal is a request that passes ordinary triage. Rejected: a proposals table, a column beside the flag, trusting the patchs line number, an escape-aware index map to buy spans for nine strings, auto-closing the flag a proposal answers.](<redacted>.md) | copy-desk / the artist loop |
|
|
324
324
|
| 0234 | [**Idea routing: capture-time promotion, the landing matrix, and the homeless-only inbox** ([task 1003064](https://cloudbongos.com/builders#/task/1003064) · goal 1000071, "Idea routing"). Idea 1000771 named the failure: Discord `#bugs` created tasks DIRECTLY against a hardcoded, already-shipped version — no `captured_by`, no lineage, so the ideator-credit lane had nothing to pay against — while every other open idea sat waiting for a human triage pass that dropped its promotion into a version catch-all, because nothing in capture ever named a goal. Records what R01/R02/R05/R07/R13 shipped. `captureIdea` (`modules/ideas/inbox.js`) stays the ONE capture chokepoint — routing is one optional `goalId` argument, never a second creation door — and when the matrix routes, `routeIdeaToGoal` writes the idea row, the task (via the `lifecycle.createTask` port) and an `ideas_task_lineage` row (`origin='route'`) in ONE transaction, so a route can never mint attribution-free work the way `#bugs` did. `modules/ideas/routing.js`'s `decideLanding` is PURE and fail-closed: a bug from a Metic+ filer hard-lands `ready`, a sub-Metic bug or any other kind with a goal soft-lands `backlog`, a Full Idea or `criterion-proposal`/`decision` never routes, no goal named stays the pre-routing inbox path. `authorizeRoutedLanding` is the BINDING gate underneath the matrix's decision — a goal reaching protected scope raises the task's rank floor to Metic and REFUSES a sub-Metic filer outright, while membership is deliberately NOT required (reporting isn't authoring). `core_222`'s `suggested_goal_id` is an advisory hint only — the real home is `tasks.goal_id`, set by construction — and `/idea-triage` (`scripts/gds/triage.js`) now sends `goal_id` on promotion, so the inbox is homeless-only. Divergence recorded: R11 (Discord `#bugs`) hard-lands a linked Metic+ builder's bug immediately through the SAME shared matrix rather than the spec's "always soft-land, pending queue" design, because a local copy of the rank split would only drift from the one definition; and R12 (Discord `#ideas` hints) is NOT YET SHIPPED, so `#ideas` carries no goal hint today despite the interview decision that it should, hint-only, never route.](<redacted>.md) | ideas / lifecycle |
|
|
325
|
+
| 0235 | [**A fix category is `kind='bug'` under a goal, not a new column** ([task 1003215](https://cloudbongos.com/builders#/task/1003215) · goal 1000071, "Idea routing" — the structural half of idea 1000828; task 1003214 shipped the advisory's first quiet case, task 1003157 extended it). A fix for something that should already work needs a home under an existing goal WITHOUT a new goal being born and WITHOUT a stored column restating what `tasks.kind` already says. Not a global "Bugs" goal either: a goal spanning every module makes the [ADR 0086](<redacted>.md) scope wall either everything or nothing, and a goal that can never reach `achieved` (a bug container never finishes) pollutes the exact signal [ADR 0183](<redacted>.md)'s auto-satisfy depends on. Not [ADR 0192 "a category orients and authorises nothing"](<redacted>.md) either, despite the structural resemblance: that decision forbids any gate from branching on a goal's category, while THIS decision is two decision surfaces deliberately branching on `kind='bug'` — `adviseGoal()` (`modules/lifecycle/goal-advisory.js`) goes silent for a bug under all three of its reasons (`no_goal`, `catch_all`, `no_criterion_link` — "a fix is the goal's 'fix category', not a delivery against one of its stated outcomes," its own doc comment) and `countTasksWithoutCriterionLink()` (`modules/lifecycle/db-deps-criteria.js`, surfaced on the hall's goal detail view) excludes `kind='bug'` from its count for the same reason. No hall/status change was needed for GROUPING bugs under a goal: every task card already renders its `kind` as a visible tag and the goal detail view's Filters already offer a `Kind` lens with `bug` as a value — verified against the live code, not built new. Rejected: a `tasks.category`/`fix_category` column or enum, a global cross-module "Bugs" goal, a bug-specific grouping widget beside the general-purpose kind facility that already works.](<redacted>.md) | ideas / lifecycle |
|
|
325
326
|
|
|
326
327
|
> ⚠️ **Numbering collisions are now machine-enforced, not narrated.** Twenty numbers are shared by two ADRs each — assigned in parallel sessions before anything checked. The files keep their filenames (renumbering would break every existing citation, and a number once assigned is never reused), and this table disambiguates each pair as `NNNN-a` / `NNNN-b`. The authoritative list is `LEGACY_DUPLICATE_ADRS` in [`scripts/gds/adr-namespace.js`](../../scripts/gds/adr-namespace.js), frozen by exact filename: a **new** collision — or a third file joining a legacy number, or a rename of either half — hard-fails the `unit` CI gate, as does an index row that points at no file or an ADR with no row. The list may only shrink. This paragraph used to narrate the collisions one by one and had fallen eight behind reality; see [ADR 0195](<redacted>.md) for why the check exists and why the pairs are grandfathered rather than renumbered.
|
|
@@ -1437,5 +1437,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1437
1437
|
landed since 1.19.498 with no explicit bump. run 33769500709. (task 1002620)
|
|
1438
1438
|
1.19.500 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1439
1439
|
landed since 1.19.499 with no explicit bump. run 33772499045. (task 1002620)
|
|
1440
|
+
1.19.501 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1441
|
+
landed since 1.19.500 with no explicit bump. run 33774087943. (task 1002620)
|
|
1442
|
+
1.19.502 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1443
|
+
landed since 1.19.501 with no explicit bump. run 33777587058. (task 1002620)
|
|
1440
1444
|
---------------------------------------------------------------------------
|
|
1441
1445
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.502",
|
|
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.502",
|
|
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.502",
|
|
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",
|
|
@@ -28,6 +28,7 @@ const { spawnSync } = require('node:child_process');
|
|
|
28
28
|
const G = require('./gen-repo-map');
|
|
29
29
|
const FM = require('./gen-file-map');
|
|
30
30
|
const SI = require('./gen-session-index');
|
|
31
|
+
const CI = require('./copy-inventory');
|
|
31
32
|
|
|
32
33
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
33
34
|
|
|
@@ -36,10 +37,18 @@ const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
|
36
37
|
// hand-written prose with one generated block; docs/file-map.md is hand-written
|
|
37
38
|
// prose with TWO generated section blocks (ADR 0066). (task 1385: session-log-index
|
|
38
39
|
// joined so a confirmed PR that only conflicts on it self-heals like the rest.)
|
|
40
|
+
// docs/copy-inventory.md + docs/copy-registry.json (task 1003543) are the copy
|
|
41
|
+
// registry's own pair — like repo-map.md, `copy-inventory.js` writes them
|
|
42
|
+
// wholesale from a tree scan with nothing hand-authored in either, so any two
|
|
43
|
+
// branches that both touch hall/status/landing copy regenerate them differently
|
|
44
|
+
// and conflict on ordinary text; they join the whole-file group for the same
|
|
45
|
+
// reason session-log-index.md did.
|
|
39
46
|
const REGENERABLE = new Set([
|
|
40
47
|
'docs/repo-map.md',
|
|
41
48
|
'docs/file-map.md',
|
|
42
49
|
'docs/session-log-index.md',
|
|
50
|
+
'docs/copy-inventory.md',
|
|
51
|
+
'docs/copy-registry.json',
|
|
43
52
|
...G.NESTED_DIRS.map((d) => `${d}/CLAUDE.md`),
|
|
44
53
|
]);
|
|
45
54
|
|
|
@@ -86,6 +95,17 @@ function regenerateResolved(relpath, { base = '', ours = '', theirs = '' } = {})
|
|
|
86
95
|
return { content: SI.renderIndex(SI.collectEntries()), ok: true };
|
|
87
96
|
}
|
|
88
97
|
|
|
98
|
+
// docs/copy-registry.json / docs/copy-inventory.md: both artifacts of one scan
|
|
99
|
+
// (copy-inventory.js), fully generated — ignore all three sides, rebuild from
|
|
100
|
+
// the merged tree. Reuse buildRegistry() rather than shelling out to write() so
|
|
101
|
+
// this stays in-process like the repo-map/session-index heals above.
|
|
102
|
+
if (relpath === 'docs/copy-registry.json') {
|
|
103
|
+
return { content: `${JSON.stringify(CI.buildRegistry(), null, 2)}\n`, ok: true };
|
|
104
|
+
}
|
|
105
|
+
if (relpath === 'docs/copy-inventory.md') {
|
|
106
|
+
return { content: CI.renderReport(CI.buildRegistry()), ok: true };
|
|
107
|
+
}
|
|
108
|
+
|
|
89
109
|
const model = G.buildModel();
|
|
90
110
|
|
|
91
111
|
// docs/repo-map.md is fully generated — ignore all three sides, rebuild it.
|
package/src/module-api.js
CHANGED
|
@@ -45,7 +45,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
45
45
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
46
46
|
// the entry to that file. Look for a version's history there, not here.
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
|
-
const CORE_VERSION = '1.19.
|
|
48
|
+
const CORE_VERSION = '1.19.502'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
49
49
|
|
|
50
50
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
51
51
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
// markers remain) so a human resolves it — the heal never clobbers real edits.
|
|
15
15
|
// 4. The merge-driver entrypoint writes %A + exits 0 (heal) / 1 (real conflict).
|
|
16
16
|
// 5. A non-generated path is never touched (not in REGENERABLE).
|
|
17
|
+
// 6. END-TO-END: a real `git merge` invokes the driver via .gitattributes.
|
|
18
|
+
// 7. .gitattributes' merge=otb-regen lines and REGENERABLE agree (task 1003543).
|
|
19
|
+
// 8. docs/copy-registry.json + docs/copy-inventory.md (task 1003543) — the copy
|
|
20
|
+
// registry's own pair — regenerate clean from the merged tree, same as
|
|
21
|
+
// repo-map.md, ignoring the conflicted sides.
|
|
22
|
+
// 9. END-TO-END for docs/copy-registry.json: two branches that each regenerate
|
|
23
|
+
// it differently merge without conflict markers via the real .gitattributes +
|
|
24
|
+
// git-config wiring, and the result equals a fresh regeneration.
|
|
17
25
|
//
|
|
18
26
|
// Run: node tests/git_merge_regen.mjs
|
|
19
27
|
|
|
@@ -30,6 +38,7 @@ const here = path.dirname(fileURLToPath(import.meta.url));
|
|
|
30
38
|
const repo = path.resolve(here, '..');
|
|
31
39
|
const regen = require('../scripts/gds/git-merge-regen.js');
|
|
32
40
|
const G = require('../scripts/gds/gen-repo-map.js');
|
|
41
|
+
const CI = require('../scripts/gds/copy-inventory.js');
|
|
33
42
|
|
|
34
43
|
const MARKERS = /^(<{7}|={7}|>{7})/m;
|
|
35
44
|
|
|
@@ -148,4 +157,82 @@ function withBlockBody(src, body) {
|
|
|
148
157
|
}
|
|
149
158
|
}
|
|
150
159
|
|
|
160
|
+
// ── 7) .gitattributes' merge=otb-regen lines and REGENERABLE agree (task 1003543) ─
|
|
161
|
+
// The whole scheme is two hand-maintained lists naming the same files (one in
|
|
162
|
+
// .gitattributes, one in REGENERABLE) — nothing checked that they still matched, so
|
|
163
|
+
// docs/copy-inventory.md + docs/copy-registry.json could be (and were) added to
|
|
164
|
+
// neither and merge like ordinary text. This pins the two lists as one set.
|
|
165
|
+
{
|
|
166
|
+
const attrsText = fs.readFileSync(path.join(repo, '.gitattributes'), 'utf8');
|
|
167
|
+
const otbRegenPaths = [...attrsText.matchAll(/^(\S+)\s+merge=otb-regen\s*$/gm)].map((m) => m[1]);
|
|
168
|
+
assert.ok(otbRegenPaths.length > 0, 'expected at least one merge=otb-regen line in .gitattributes');
|
|
169
|
+
for (const p of otbRegenPaths) {
|
|
170
|
+
assert.ok(regen.REGENERABLE.has(p), `.gitattributes declares merge=otb-regen for ${p} but REGENERABLE (git-merge-regen.js) does not know it`);
|
|
171
|
+
}
|
|
172
|
+
for (const p of regen.REGENERABLE) {
|
|
173
|
+
assert.ok(otbRegenPaths.includes(p), `REGENERABLE (git-merge-regen.js) knows ${p} but .gitattributes has no merge=otb-regen line for it`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── 8) docs/copy-registry.json + docs/copy-inventory.md regenerate clean ────────
|
|
178
|
+
// (task 1003543 — the copy registry's own pair, from copy-inventory.js. Both
|
|
179
|
+
// artifacts of one scan, fully generated — like docs/repo-map.md, all three
|
|
180
|
+
// conflicted sides are ignored and the merged tree is rebuilt from scratch.)
|
|
181
|
+
{
|
|
182
|
+
const freshReg = CI.buildRegistry();
|
|
183
|
+
const freshJson = `${JSON.stringify(freshReg, null, 2)}\n`;
|
|
184
|
+
const freshMd = CI.renderReport(freshReg);
|
|
185
|
+
|
|
186
|
+
const rJson = regen.regenerateResolved('docs/copy-registry.json', {
|
|
187
|
+
base: '{}\n', ours: '<<<<<<< ours\n{"a":1}\n=======\n{"b":2}\n>>>>>>> theirs\n', theirs: 'nonsense\n',
|
|
188
|
+
});
|
|
189
|
+
assert.equal(rJson.ok, true, 'copy-registry.json heal must succeed');
|
|
190
|
+
assert.ok(!MARKERS.test(rJson.content), 'copy-registry.json heal must leave no conflict markers');
|
|
191
|
+
assert.equal(rJson.content, freshJson, 'copy-registry.json heal must equal a fresh regeneration');
|
|
192
|
+
|
|
193
|
+
const rMd = regen.regenerateResolved('docs/copy-inventory.md', {
|
|
194
|
+
base: '# base\n', ours: '<<<<<<< ours\n# a\n=======\n# b\n>>>>>>> theirs\n', theirs: 'nonsense\n',
|
|
195
|
+
});
|
|
196
|
+
assert.equal(rMd.ok, true, 'copy-inventory.md heal must succeed');
|
|
197
|
+
assert.ok(!MARKERS.test(rMd.content), 'copy-inventory.md heal must leave no conflict markers');
|
|
198
|
+
assert.equal(rMd.content, freshMd, 'copy-inventory.md heal must equal a fresh regeneration');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── 9) END-TO-END: two branches that both regenerate docs/copy-registry.json ────
|
|
202
|
+
// differently merge clean via the real .gitattributes + git-config wiring — the
|
|
203
|
+
// exact PR 526/531 shape from task 1003543 (two hall PRs both ran copy-inventory.js
|
|
204
|
+
// concurrently and conflicted as ordinary JSON/markdown). Mirrors test 6 above.
|
|
205
|
+
{
|
|
206
|
+
const driver = path.join(repo, 'scripts/gds/git-merge-regen.js');
|
|
207
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-gitmerge-copyreg-'));
|
|
208
|
+
const g = (...a) => {
|
|
209
|
+
const r = spawnSync('git', a, { cwd: tmp, encoding: 'utf8' });
|
|
210
|
+
return { status: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
|
|
211
|
+
};
|
|
212
|
+
const writeReg = (s) => fs.writeFileSync(path.join(tmp, 'docs', 'copy-registry.json'), s);
|
|
213
|
+
try {
|
|
214
|
+
g('init', '-q');
|
|
215
|
+
g('config', 'user.email', 't@t'); g('config', 'user.name', 't');
|
|
216
|
+
g('config', 'merge.otb-regen.driver', `node ${driver} %O %A %B %P`);
|
|
217
|
+
fs.writeFileSync(path.join(tmp, '.gitattributes'), 'docs/copy-registry.json merge=otb-regen\n');
|
|
218
|
+
fs.mkdirSync(path.join(tmp, 'docs'));
|
|
219
|
+
writeReg('{"schema":0}\n'); g('add', '-A'); g('commit', '-qm', 'base');
|
|
220
|
+
g('checkout', '-qb', 'feature');
|
|
221
|
+
writeReg('{"schema":0,"feature":true}\n'); g('add', '-A'); g('commit', '-qm', 'feat');
|
|
222
|
+
g('checkout', '-q', 'master');
|
|
223
|
+
if (g('rev-parse', '--verify', 'master').status !== 0) g('checkout', '-q', 'main');
|
|
224
|
+
writeReg('{"schema":0,"master":true}\n'); g('add', '-A'); g('commit', '-qm', 'mast');
|
|
225
|
+
const merge = g('merge', '--no-edit', 'feature');
|
|
226
|
+
assert.equal(merge.status, 0, `real git merge must auto-resolve via the driver; got ${merge.status} :: ${merge.out}`);
|
|
227
|
+
const unmerged = g('diff', '--name-only', '--diff-filter=U').out.trim();
|
|
228
|
+
assert.equal(unmerged, '', `no file may be left unmerged; got: ${unmerged}`);
|
|
229
|
+
const healed = fs.readFileSync(path.join(tmp, 'docs', 'copy-registry.json'), 'utf8');
|
|
230
|
+
assert.ok(!MARKERS.test(healed), 'merged copy-registry.json must have no conflict markers');
|
|
231
|
+
assert.equal(healed, `${JSON.stringify(CI.buildRegistry(), null, 2)}\n`,
|
|
232
|
+
'merged copy-registry.json must equal a fresh regeneration from the (real) merged tree');
|
|
233
|
+
} finally {
|
|
234
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
151
238
|
console.log('git_merge_regen: all assertions passed');
|