@bongos/core 1.19.1031 → 1.19.1032

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 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.1031",
6
- "core_contract": "1.19.1031",
7
- "source_commit": "04f5126869533ad9e3a29fa072dd288ba6e63f3b",
5
+ "core_version": "1.19.1032",
6
+ "core_contract": "1.19.1032",
7
+ "source_commit": "016ed2163285ec64cc802cfe18a8990f9549c8d6",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-25T21:32:54.328Z",
9
+ "built_at": "2026-09-25T21:42:52.123Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 529,
12
+ "docs_redacted": 530,
13
13
  "agent_docs_stubbed": 26,
14
14
  "functional_verbatim": 2459,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 3014,
20
- "tree_sha256": "05efd2e30efe440d0892c97889cdea81127c2544ef93366c2b054750750fceb3",
19
+ "file_count": 3015,
20
+ "tree_sha256": "dda9c12ae3e87decfa28f7379633ebbc1c679833d2aba34708a70e4898fa7d49",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -1027,7 +1027,7 @@
1027
1027
  {
1028
1028
  "path": "docs/adr/0110-untrack-whole-file-generated-nav-docs.md",
1029
1029
  "mode": "0000644",
1030
- "sha256": "65b97416e5825bc1993f40bb041f42cd65b4ac695c2661df9d4e07077c1f90d2"
1030
+ "sha256": "ab6173c6ca1d2f37e87761dab79f011865409521bee5cd94107c33c26148677b"
1031
1031
  },
1032
1032
  {
1033
1033
  "path": "docs/adr/0111-instance-hosting-provisioning-module.md",
@@ -2184,10 +2184,15 @@
2184
2184
  "mode": "0000644",
2185
2185
  "sha256": "b64fbfec7c6bda4ec654cb89a31878d401ab4c9ea5401d3129b74cc5627edecd"
2186
2186
  },
2187
+ {
2188
+ "path": "docs/adr/0338-modules-travel-through-a-bongos-hosted-store.md",
2189
+ "mode": "0000644",
2190
+ "sha256": "5acb8ac6e656a909ef6392f845f7a479d44d7b7619197e9f66b4879e16a6971c"
2191
+ },
2187
2192
  {
2188
2193
  "path": "docs/adr/README.md",
2189
2194
  "mode": "0000644",
2190
- "sha256": "0a22c2623b9a4f8daa0235d9f7b99089300aadaca2e66119ce3f31822b9d7797"
2195
+ "sha256": "c21920e3e14d6d4d26bf7bdf830d4529686c0ec41bf0c41cdae17971fd7cfba7"
2191
2196
  },
2192
2197
  {
2193
2198
  "path": "docs/api-reference.md",
@@ -2757,7 +2762,7 @@
2757
2762
  {
2758
2763
  "path": "docs/module-api-changelog.md",
2759
2764
  "mode": "0000644",
2760
- "sha256": "6f37eb2ce46310219bfc4f0c4820e67f6afda57cfe76cb2a0e8cf75d3bd6577d"
2765
+ "sha256": "02e3773cd062329373089c27d7878ecf9134914829c464adcb72499b5dfa7474"
2761
2766
  },
2762
2767
  {
2763
2768
  "path": "docs/modules-contract.md",
@@ -5862,7 +5867,7 @@
5862
5867
  {
5863
5868
  "path": "modules/lifecycle/db-claim-reads.js",
5864
5869
  "mode": "0000644",
5865
- "sha256": "f3e757a8753eba180c3c54a5edd3199db7853162b66ffd5bf37b85240890aa1f"
5870
+ "sha256": "2f51bbe36717c80645ed263502fa26109767d3b0716f233db169cfe9ef8dfd92"
5866
5871
  },
5867
5872
  {
5868
5873
  "path": "modules/lifecycle/db-claims.js",
@@ -8362,12 +8367,12 @@
8362
8367
  {
8363
8368
  "path": "package-lock.json",
8364
8369
  "mode": "0000644",
8365
- "sha256": "eb24a6596bec145cc1e850a0c17f5f9a5a05bf4b5762785c063ac9711788417e"
8370
+ "sha256": "a101d5c7db8bf74c23437aeb893c14774bf675250489aa0f8da691c840f141d5"
8366
8371
  },
8367
8372
  {
8368
8373
  "path": "package.json",
8369
8374
  "mode": "0000644",
8370
- "sha256": "84bad55379e5b9c61129a8d4cae7b074f22ba99984543dd1ac990688ac73efcb"
8375
+ "sha256": "3267957ac4cec25809715c70027bb2c0b08cea10468cdd49dfbd0dd13f3d6d1d"
8371
8376
  },
8372
8377
  {
8373
8378
  "path": "public-docs/index.html",
@@ -10412,7 +10417,7 @@
10412
10417
  {
10413
10418
  "path": "src/module-api.js",
10414
10419
  "mode": "0000644",
10415
- "sha256": "320ce97feaea53ab38c025dbba20001ef89643b0519151e010c7456e0dd727eb"
10420
+ "sha256": "42f07ce15936b5d07b52f4cb0d582af2f0b2c248bf96240bec8ba224d18af95b"
10416
10421
  },
10417
10422
  {
10418
10423
  "path": "src/module-loader/catalog.js",
@@ -12272,7 +12277,7 @@
12272
12277
  {
12273
12278
  "path": "tests/grade_diff_hash.mjs",
12274
12279
  "mode": "0000644",
12275
- "sha256": "170b816aa9032e6eccba415f67ce60c10da5f2f706a1b1ee458a5d8578679535"
12280
+ "sha256": "7dc3ce65bada73216c06eda8ac2b10c248f1909e0953a4737584a8d851b743e4"
12276
12281
  },
12277
12282
  {
12278
12283
  "path": "tests/grade_server_authoritative.mjs",
@@ -47,3 +47,29 @@ Apply the atlas pattern to the two **whole-file navigation** artifacts — `docs
47
47
  - **Post-main regen bot / cron that re-commits the files** (the owner's two original ideas) — rejected as the *primary* mechanism: both keep the files in git, weaken the freshness gate for every content PR, burn CI on regen PRs, and (there is no GitHub webhook in this codebase) can only poll anyway. "Don't commit the file" is strictly simpler than "automate re-committing it." The bounded bot survives only for the API spec (a follow-up task), which has external consumers.
48
48
  - **Strengthen the reactive heal only** (ADR 0082++) — rejected: makes conflicts invisible, not absent; the per-branch bookkeeping commits and the heal latency remain, and the cost grows with parallelism.
49
49
  - **Trim `docs/repo-map.md` from `REGENERABLE`/`.gitattributes`** — deferred: it would force a rewrite of `tests/git_merge_regen.mjs` (which uses repo-map as its canonical whole-file heal fixture) for no real gain, since a gitignored file never reaches the driver anyway. Leaving the entries in place is the lower-risk transition posture.
50
+
51
+ ## Amendment — 2026-09-25 (task 1001929): `docs/file-map.md` stays tracked; the reactive heal is enough
52
+
53
+ **The open question (pt.4).** `docs/file-map.md` is hybrid — hand-written prose plus two generated blocks (`.claude/skills/`, `.claude/scheduled-tasks/`, [ADR 0066](0066-generated-file-map-sections.md)) — so it cannot be gitignored like the two whole files above. What was left to decide: does its generated churn cause enough parallel-branch conflicts to justify the repo-map treatment (move the blocks into a gitignored companion file and split the gate into `--check-block`/`--check-whole`), or does the `otb-regen` heal it already has (`.gitattributes` + `REGENERABLE`, [ADR 0082](0082-server-side-merge-conflict-auto-resolution.md)) cope?
54
+
55
+ **Measured, not estimated.** Every merge commit reachable from `origin/main` at `331569b4` (2026-09-25) — the whole history since the 2026-07-03 core extraction, PR landings and the branch-side "merge origin/main into task-N" freshness merges alike — was replayed with `git merge-tree --write-tree --name-only <parent1> <parent2>` in a mirror clone (`git clone --mirror --shared`) that has **no** `merge.otb-regen.driver` registered. Git then falls back to its plain 3-way text merge, so the replay shows what each merge would have hit with no heal at all. For each `file-map.md` conflict, the conflict markers in the written tree were located against the `BEGIN`/`END GENERATED FILE-MAP` markers.
56
+
57
+ | Replay result | Count |
58
+ |---|---|
59
+ | Merges replayed | 2,287 |
60
+ | Merges that would have conflicted on any file | 248 |
61
+ | … on `docs/file-map.md` | **3** — all in hand-written prose, **0** inside a generated block |
62
+ | … on `docs/file-map.notes.json` (the hand-written notes sidecar) | 0 |
63
+ | Non-merge commits touching `docs/file-map.md` | 129 — 89 prose only, 30 generated block only, 9 both |
64
+
65
+ For scale, the same replay counted 78 conflicts on `docs/copy-inventory.md`, 77 on `CLAUDE.md`, 70 on `docs/copy-registry.json`, and 21 each on `docs/api/openapi.json` and `docs/api-reference.md`.
66
+
67
+ The three `file-map.md` conflicts all fell on 2026-08-29 (the freshness merges for tasks 1002968, 1002290 and 1002967), and all have one shape: two sessions each hand-appended a line for a new test file at the same end of the `tests/` listing. There have been none since. The generated blocks do churn (39 commits changed one) but never conflicted, even with the driver switched off: each block lists one directory per line in sorted order, so two branches adding different skills usually edit different lines. If they ever do meet, the driver already re-renders both blocks from the merged tree.
68
+
69
+ **Decision: no split.**
70
+
71
+ 1. The split targets the one part of the file that has never conflicted. Moving the blocks out would have prevented none of the three conflicts on record — all were prose, and the prose stays in git whatever happens to the blocks.
72
+ 2. It would cost real machinery for no measured gain: a new gitignored file with its own regeneration on every refresh path (the recipe in Consequences above, plus the standalone path, [ADR 0147](0147-standalone-live-docs-regen.md)), a gate split, and relinking the map to the companion file.
73
+ 3. The file-map gate is unchanged: plain `gen-file-map.js --check`, hard, in `fitness.js`. It also enforces the note-per-skill contract and module coverage (task 1004066), both of which assume the file is tracked.
74
+
75
+ **Revisit if** a re-run of the same replay shows conflicts *inside* a generated block, or prose conflicts become routine. The prose case would need a different fix (for example, a per-area file for the hand-written tree); the split considered here cannot reach it.
@@ -0,0 +1,119 @@
1
+ # ADR 0338 — Modules travel through a Bongos-hosted store, and an acquired module keeps working forever
2
+
3
+ - **Status:** accepted
4
+ - **Date:** 2026-09-25
5
+ - **Task:** [task 1003782](https://cloudbongos.com/builders#/task/1003782) (goal 1000091 — working area 5, Module distribution & economy)
6
+ - **Deciders:** Will (owner of working area 5) chose the transport and the delist semantics; Claude researched the options and wrote the record.
7
+ - **Related:** [ADR 0264](0264-the-ten-working-areas.md) (area 5's state) · [ADR 0107](0107-module-upstreaming-contribution-to-core-catalog.md) §6 (modules ship inside core today) · [ADR 0135](0135-module-upstream-submission-interim-queue.md) (the interim submit queue) · [ADR 0138](0138-bundled-module-deps-declared-per-module.md) (module npm deps composed at init) · [ADR 0161](0161-publish-on-merge.md) / [ADR 0283](0283-the-core-publishes-publicly-because-access-was-never-the-thing-protecting-it.md) (how the core itself is distributed) · [ADR 0324](0324-the-public-npm-cli-channel-is-retired.md) (the owner retiring a public npm channel) · [ADR 0098](0098-public-mirror-publish-manifest.md) (`isPublishable()`)
8
+
9
+ ## Context
10
+
11
+ A module has no way to travel on its own. `bongos module list` enumerates what is on disk,
12
+ `bongos module submit` pushes *into* core (ADR 0135), and nothing installs, fetches or
13
+ downloads one. A module reaches an instance only by shipping inside the core package
14
+ (`default:false`, ADR 0107 §6) or by living in the instance's own repo. A module also has no
15
+ version of its own — `module.json` carries only a `coreVersion` range (task 1003781 adds one).
16
+
17
+ Roughly a third of area 5 hangs off how a module travels: install (task 1003785), update
18
+ (task 1003786), upgrade reporting (task 1003787), the registry and entitlement tables
19
+ (tasks 1003812, 1003813), release channels (task 1003815) and delist (task 1003797). Whatever
20
+ is chosen has to leave room for four things the area's criteria already require:
21
+
22
+ 1. an **entitlement check before install** — a paid module is not handed to someone who has not paid;
23
+ 2. a **per-version quality score**;
24
+ 3. a **tester channel** that sees a version before general release;
25
+ 4. a **delist** that actually stops distribution.
26
+
27
+ Three transports were weighed:
28
+
29
+ | | npm packages | Bongos-hosted store | Git refs |
30
+ |---|---|---|---|
31
+ | Added monthly cost | $0 | ≈ $0 at first (existing control plane + the $5/mo DO Spaces bucket already paid for backups) | $0 |
32
+ | Entitlement before install | **No** — a public package is downloadable by anyone; a private one needs a paid npm seat per buyer | **Yes** — the server checks before it serves | Only by per-repo access management |
33
+ | Delist stops distribution | **No** — npm restricts unpublish, and published versions persist | **Yes** — the server stops serving | **No** — clones and forks persist |
34
+ | Tester channel | dist-tags, awkwardly | native | branches, awkwardly |
35
+ | Integrity | built in | reuse the core's `tree_sha256` manifest | weak |
36
+ | Upkeep | another public channel (ADR 0324 retired the last one on the owner's judgement) | we run upload/download | low |
37
+
38
+ npm and git refs both fail requirements 1 and 4 by construction, and those two are the
39
+ economy: a store that cannot withhold a paid module or stop distributing a removed one is not
40
+ a store. That is the point of this working area, and the owner said so directly.
41
+
42
+ ## Decision
43
+
44
+ ### D1 — The transport is a Bongos-hosted module store
45
+
46
+ Cloud Bongos hosts the store. An author publishes a packaged module version to it; an
47
+ instance acquires a module from it; people pay for modules through it (the money movement
48
+ itself is area 8's, per the area-5 criteria — this ADR decides the channel, not the payment).
49
+
50
+ The shape the implementing tasks build to:
51
+
52
+ - **Artifact.** A module version is a tarball plus a manifest in the same format
53
+ `package-core.js` already produces for the core: per-file hashes and a
54
+ compression-independent `tree_sha256`, built through the same `isPublishable()`
55
+ default-deny gate (ADR 0098). No second packaging format.
56
+ - **Where it lives.** The registry rows (module, version, price, channel, score) are tables on
57
+ the control-plane database (tasks 1003812, 1003814, 1003815). The tarballs live in object
58
+ storage — the DO Spaces bucket the platform already pays for — or on the control plane's
59
+ disk while volume is small. The store is served by the control plane.
60
+ - **Acquire = check, then serve.** Download is an authenticated request; the server checks the
61
+ entitlement (task 1003813) and the channel the instance is on before it hands over the
62
+ artifact. A free module still goes through the same path with a zero price, so free and paid
63
+ are one code path (the owner's "assessed identically" rule applies to distribution too).
64
+ - **Verify on arrival.** `bongos module install` / `update` (tasks 1003785, 1003786) verify
65
+ the tarball against its manifest hashes before placing it under the loader's discovery root,
66
+ and record the exact version acquired.
67
+ - **Modules' own npm dependencies** keep travelling through npm as ADR 0138 already composes
68
+ them. The store carries the module's own code, not its third-party libraries.
69
+
70
+ **Cost.** This adds no new fixed cost today. If download volume outgrows the Spaces tier's
71
+ included transfer, that is the point to revisit, with serving from the control plane's disk as
72
+ the cheaper fallback (CLAUDE.md §4).
73
+
74
+ ### D2 — An acquired module keeps working, forever; removal only stops what comes next
75
+
76
+ When a module is **delisted** by Metic+ (task 1003797), **withdrawn by its author**, or its
77
+ store entry is otherwise deleted:
78
+
79
+ - it **leaves the catalog** and **cannot be newly acquired**;
80
+ - **no further versions or updates** are served for it;
81
+ - **every instance that already acquired it keeps it, and it keeps working** — the store never
82
+ reaches into an instance to disable, remove or expire an acquired module, and an entitlement
83
+ is never revoked by a delist;
84
+ - the instance is **told** — `bongos upgrade` / the module update check (task 1003787) and the
85
+ hall's module view say plainly that this module will receive no more updates.
86
+
87
+ The artifact an instance acquired is already on that instance, so this costs the store nothing
88
+ to honour: it simply never deletes or disables what it has handed over.
89
+
90
+ The refund path in task 1003789 is the one place an entitlement can end, and it is a separate,
91
+ buyer-initiated act — a delist is not a refund.
92
+
93
+ ## Consequences
94
+
95
+ - **Install, update and upgrade (1003785–1003787) are unblocked** against a concrete target:
96
+ an authenticated control-plane download of a hash-manifested tarball.
97
+ - **Delist (1003797) is a catalog and serving change only.** It must not touch installed
98
+ copies; a test should pin that a delisted module still loads on an instance that holds it.
99
+ - **Security issues are not a special case under D2.** The owner's rule is "always works; tell
100
+ the user". A module with a vulnerability is delisted and the holder is warned; the store does
101
+ not remotely disable it. If a remote kill switch is ever wanted, that needs a new ADR that
102
+ supersedes D2 — it is not implied here.
103
+ - **The store becomes something we run.** Upload, download, storage and its availability are
104
+ now platform responsibilities on the control plane. An outage blocks new installs and
105
+ updates; it never affects an already-installed module, by D2.
106
+ - **ADR 0135's per-instance submission queue** remains the path for proposing a module
107
+ *into core*. Publishing to the store is a different act (task scope for the publish command
108
+ is not yet filed and should be, alongside 1003785).
109
+ - **No cryptographic signing yet.** Integrity is the manifest hashes, delivered over TLS from
110
+ a server we control. Author signing can be added later without changing the transport.
111
+
112
+ ## Rejected
113
+
114
+ - **npm packages** — public packages cannot withhold a paid module or be truly withdrawn, and
115
+ the owner has already retired one public npm channel (ADR 0324) on the judgement of what the
116
+ project wants to maintain.
117
+ - **Git refs** — cheapest to start, but no integrity story, no ranking hook, and a removal
118
+ cannot stop copies already cloned.
119
+ - **Remote disable on delist** — rejected by the owner: getting a module means keeping it.
@@ -429,3 +429,4 @@ This keeps the decision history honest and traceable.
429
429
  | 0335 | [**An invite is a pre-approved row on the project's own instance, and `project.recruit` says who may send one** ([task 1002813](https://cloudbongos.com/builders#/task/1002813), R1 of the project admin console wave, goal 1000110 — BONGOS-V2). Supersedes ADR 0141 §5's "Invitations (deferred slice)" paragraph only. Formalises spike task 1002786's three decisions and reconciles them with six weeks of shipping: the instance invite route (task 1003044), the PATCH rescind (ADR 0201 D4.2) and the composed join door (ADR 0247) already exist, and the hub invite's dead end is now reachable from the creation wizard. **Decisions:** (D1) authority lives on the project's own instance, and the instance IS the project; the hub is identity, relay, echo, notification and links, and never writes a project's admission row; (D2) a recruit invite is an `access_requests` row with `status='invited'`, so signing in is the acceptance act and the gate is untouched; the row gains `kind ∈ (application, invite)` and `created_by` (the inviter, which a rescind would otherwise overwrite), reuses `note`, backfills exactly (`created_at = resolved_at`), and neither column ever enters an admission read; (D3) one new grantable atom, `project.recruit` (`system:false`, floor archon, widened per instance through the existing governance tab), guards the invite, the outstanding list, the invite rescind and the queue read, while `access_request.review` keeps the verdict; because inviting is admitting, a recruiter without the review atom may not approve or re-admit an APPLICANT through the invite route (409 to the queue); the hub sliver moves onto the atom, and the hub directory and hub invite KEEP `project.curate`, revising ADR 0210/0215/0248, since swapping the directory would raise a gate on a viewing surface (ADR 0217). **Task map:** R2 (task 1002814) = the migration, the invite writes and the sender list, still on `access_request.review`; R3 (task 1002815) = mint the atom, move the gates, the 409 clause, the sliver swap and the pins, and it lands after R2; R5 (task 1002817) is already delivered, so close it into task 1002331; task 1003362 folds into R3. Rejected: a new invitations table; the hub pending row as the invite; widening `access_request.review` instead of minting an atom; a per-project settings row for the floor; letting a recruiter's invite approve applications; `system:true`; swapping `project.curate` on the hub directory.](0335-an-invite-is-a-pre-approved-row-on-the-projects-own-instance.md) | governance / onboarding / platform-identity |
430
430
  | 0336 | [**A guild is a hub-side group whose members each consent twice, and its collective record is only ever the sum of what those members already show** ([task 1002299](https://cloudbongos.com/builders#/task/1002299), goal 1000110 — the R23 design spike for goal 1000045's C7/C8/C9). No platform guild existed (every "guild" in the tree is Discord's). Decisions: (1) three hub tables in `platform-identity`, in their own `guilds.js` (the 1,500-line ratchet), distinct from instance-local `goal_members`: guilds (slug, name, `visibility ∈ public|unlisted` with **no default**), members (`membership_kind ∈ owner|member`, never `role` per ADR 0174; a partial unique index holds exactly one owner; a per-member `shown_publicly`), and requests; (2) two consents to join, no self-join, through ADR 0202's connections lifecycle verbatim (silent decline, 90-day swallowed re-ask, 30-day read-time expiry); `accountActiveSql` floors the feature; an **unlisted guild 404s to everyone on the public read, members included**, and takes invites only, because a slug is guessable; (3) the public roster is `shown_publicly AND accountVisibleSql`, and widening a guild to public changes no member's flag but the flipping owner's, so consent given to a small audience is never reused for a large one; (4) **the privacy resolution — NOT ratified; superseded by [ADR 0337](0337-the-hub-account-is-the-person-a-hall-account-is-their-seat-in-one-project.md) D4.4:** it proposed that a member is counted iff shown AND `rollupVisibleSql`, read-time, `no-store`, never stored, and N and M are both lengths of lists the reader can see. The guild record is therefore always a sum of numbers already on members' own public profiles, so nothing can be subtracted out. All-or-nothing is rejected (one member blanks everyone and hiding becomes costly), as is per-member consent to count hidden numbers (it breaks "hide_stats beats everything"). The trade-off: guilds of privacy-minded builders look smaller and rank lower. A union count of all members' projects is refused because it reveals shared private projects; (5) the namespace is `/g/<slug>`, `g` is reserved in the SEO spec's project-slug list, the slug is fixed in V1, and `tests/guild_namespace_reserved.mjs` pins the two documents together; (6) a guild engages a project only through the individual door: a guild application is each member's own vouched application, labelled through ADR 0260's live applicant-profile read (no new instance field); inviting a guild fans out ordinary individual invites to its public roster at that moment, **never a guild-level pre-approval** (that would let a guild owner decide admission to someone else's project); (7) the map ranks public guilds by size and counted works shipped, read-time, with a slug tiebreak, and R29 narrows to the visual. Scope changes for R24–R37 are tabled in the ADR, including R28's dependency on the abandoned R11 and R29/R31's archived orb goals.](0336-a-guilds-record-is-the-sum-of-what-its-members-already-show.md) | platform identity / privacy / social graph |
431
431
  | 0337 | [**The hub account is the person, a hall account is their seat in one project, so anything that spans projects lives on the hub** ([task 1004255](https://cloudbongos.com/builders#/task/1004255), goal 1000110 — the owner's hub/hall re-scope, 2026-09-24/25). Extends [ADR 0141](<redacted>.md) from identity to placement: (1) a surface about the person across projects, or about more than one project, goes on the HUB (`public-landing`, `platform-identity`): people directory, community, guilds, connections, recruiting, platform-account settings; one about this project goes in the HALL (`hall-ui`); "Builders you may know" is on both; `status-ui` is the status dashboard, not the hall; (2) no authority moves, and hub actions still land as the project's own rows; (3) hall `/people` retires, ADR 0334's recruiting directory moves unchanged to the hub community page, `/roster` is the in-project directory; (4) owner calls: the hidden-builder recruiter panel is recruiter-only while public search is unchanged (amends community-tab spec D8); platform-account settings get a new hub page; **guild totals count hidden and private members by default, with a notice on the join card and a per-guild off switch (`counted_in_totals`), rounded to a 1–2–5 band, only once a guild has at least 3 of them, refreshed weekly; size stays the public roster length** (supersedes ADR 0336 D5; relaxes the privacy spec's "`hide_stats` beats everything" for guild totals only, for a member who was told), with the fake-account padding limit stated and accepted.](0337-the-hub-account-is-the-person-a-hall-account-is-their-seat-in-one-project.md) | platform identity / placement / privacy |
432
+ | 0338 | [**Modules travel through a Bongos-hosted store, and an acquired module keeps working forever** ([task 1003782](https://cloudbongos.com/builders#/task/1003782), goal 1000091 — working area 5, owner Will). A module had no transport of its own: it reached an instance only inside the core (ADR 0107 §6) or in the instance's repo. Weighed npm packages, a Bongos-hosted store and git refs against the four things area 5's criteria require — entitlement before install, a per-version score, a tester channel, and a delist that stops distribution. npm and git refs fail entitlement and delist by construction. **D1:** Cloud Bongos hosts the store; a module version is a tarball + hash manifest in the same format `package-core.js` already emits (same `isPublishable()` gate), registry rows on the control-plane DB, artifacts in the already-paid DO Spaces bucket, served by the control plane only after an entitlement + channel check; free modules take the same path at price zero; install/update verify hashes on arrival; modules' own npm deps still travel via npm (ADR 0138). No new fixed cost. **D2 (owner):** delisting, author withdrawal or deletion removes a module from the catalog and stops new acquisitions and updates, but **an acquired module keeps working forever** — the store never disables or removes an installed copy and a delist never revokes an entitlement; the holder is told no more updates will come. Security issues are not a special case; a remote kill switch would need a superseding ADR. Rejected: npm, git refs, remote disable on delist.](0338-modules-travel-through-a-bongos-hosted-store.md) | modules / distribution / economy |
@@ -2541,5 +2541,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2541
2541
  landed since 1.19.1029 with no explicit bump. run 36190779629. (task 1002620)
2542
2542
  1.19.1031 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2543
2543
  landed since 1.19.1030 with no explicit bump. run 36192093201. (task 1002620)
2544
+ 1.19.1032 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2545
+ landed since 1.19.1031 with no explicit bump. run 36192968150. (task 1002620)
2544
2546
  ---------------------------------------------------------------------------
2545
2547
  ```
@@ -157,8 +157,14 @@ async function getLastClaimHolder(taskId) {
157
157
  // /grade-recover reads a failed grade as having no findings at all. Lifting is
158
158
  // additive: `signals.issues` stays exactly where it is for anything already
159
159
  // reading it.
160
- async function getTaskGrade(taskId) {
161
- const { rows } = await pool.query(
160
+ //
161
+ // `deps.pool` is the family's test-only pool seam (task 538), honoured ONLY under
162
+ // NODE_ENV==='test' (the same line moduleScopeForActiveClaims uses above), so
163
+ // tests/grade_diff_hash.mjs can drive the lift with a fake row instead of a
164
+ // real Postgres (task 1003490). Production callers pass the task id alone.
165
+ async function getTaskGrade(taskId, deps = {}) {
166
+ const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
167
+ const { rows } = await activePool.query(
162
168
  `SELECT diff_hash, passed, score, threshold, grader_kind, grader_version,
163
169
  rubric_scores, signals, notes_md
164
170
  FROM task_grades
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.1031",
3
+ "version": "1.19.1032",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.1031",
9
+ "version": "1.19.1032",
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.1031",
3
+ "version": "1.19.1032",
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/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.1031'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.1032'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -5,8 +5,9 @@
5
5
  // Covers:
6
6
  // 1. diffHash / shouldReuseGrade — pure helpers in ship.js (no git/network)
7
7
  // 2. db.applyGrade stores diff_hash — SQL-aware fakePool (NODE_ENV=test seam)
8
- // 3. db.getTaskGrade export exists and returns null for a nonexistent task
9
- // (full integration requires a real DB; structural contract is asserted here)
8
+ // 3. db.getTaskGrade — exported, returns null for a task with no grade, and
9
+ // lifts signals.issues to the top level without removing the nested key
10
+ // (task 1003490; driven through the NODE_ENV=test deps.pool seam)
10
11
  //
11
12
  // Run: node tests/grade_diff_hash.mjs
12
13
 
@@ -210,6 +211,80 @@ await test('getTaskGrade is exported from lifecycle/db.js', () => {
210
211
  assert.equal(typeof db.getTaskGrade, 'function', 'getTaskGrade must be exported');
211
212
  });
212
213
 
214
+ // task 1003490 — the issues LIFT (task 1003488's second half). task_grades has
215
+ // no `issues` column, so applyGrade stores the findings under signals.issues;
216
+ // getTaskGrade copies them to the top level so a reader that does not know that
217
+ // storage detail still sees them. These drive the real function through the
218
+ // deps.pool seam (honoured because NODE_ENV === 'test' above) with a fake row,
219
+ // so dropping or inverting the lift turns this file red.
220
+ function gradeRowPool(row) {
221
+ const calls = [];
222
+ return {
223
+ calls,
224
+ async query(sql, params) {
225
+ calls.push({ sql, params });
226
+ return { rows: row === undefined ? [] : [row] };
227
+ },
228
+ };
229
+ }
230
+ function storedGradeRow(signals) {
231
+ return {
232
+ diff_hash: 'abc123', passed: false, score: 4.5, threshold: 6,
233
+ grader_kind: 'subagent-panel', grader_version: 'v4.0',
234
+ rubric_scores: { functional_fidelity: 9, code_quality: 2 },
235
+ signals, notes_md: 'FAIL',
236
+ };
237
+ }
238
+
239
+ await test('getTaskGrade lifts signals.issues onto the returned row', async () => {
240
+ const stored = [
241
+ { severity: 'blocker', file: 'a.js', summary: 'breaks the build', worker: 'quality' },
242
+ { severity: 'nit', file: 'b.js', summary: 'naming', worker: 'narc' },
243
+ ];
244
+ const pool = gradeRowPool(storedGradeRow({ panel_outcome: 'fail', issues: stored }));
245
+ const grade = await db.getTaskGrade(42, { pool });
246
+ assert.deepEqual(grade.issues, stored, 'the stored findings appear at the top level');
247
+ assert.equal(pool.calls.length, 1, 'one read');
248
+ assert.match(pool.calls[0].sql, /FROM task_grades/);
249
+ assert.deepEqual(pool.calls[0].params, [42], 'keyed on the task id');
250
+ });
251
+
252
+ await test('getTaskGrade leaves signals.issues in place (the lift is additive)', async () => {
253
+ const stored = [{ severity: 'major', file: 'c.js', summary: 'unchecked input' }];
254
+ const grade = await db.getTaskGrade(42, {
255
+ pool: gradeRowPool(storedGradeRow({ panel_outcome: 'fail', issue_count: 1, issues: stored })),
256
+ });
257
+ assert.deepEqual(grade.signals.issues, stored, 'signals.issues is still there for readers of the nested key');
258
+ assert.equal(grade.signals.panel_outcome, 'fail', 'the rest of signals is untouched');
259
+ assert.equal(grade.signals.issue_count, 1);
260
+ assert.equal(grade.score, 4.5, 'the row columns ride through unchanged');
261
+ assert.deepEqual(grade.rubric_scores, { functional_fidelity: 9, code_quality: 2 });
262
+ });
263
+
264
+ await test('getTaskGrade yields issues: [] (not undefined) when signals carries no issues', async () => {
265
+ const grade = await db.getTaskGrade(42, { pool: gradeRowPool(storedGradeRow({ panel_outcome: 'pass' })) });
266
+ assert.ok(Array.isArray(grade.issues), `issues must be an array, got ${typeof grade.issues}`);
267
+ assert.deepEqual(grade.issues, []);
268
+ });
269
+
270
+ await test('getTaskGrade yields issues: [] when signals itself is null', async () => {
271
+ const grade = await db.getTaskGrade(42, { pool: gradeRowPool(storedGradeRow(null)) });
272
+ assert.deepEqual(grade.issues, []);
273
+ assert.equal(grade.signals, null, 'a null signals column is passed through, not invented');
274
+ });
275
+
276
+ for (const [label, malformed] of [['a string', 'blocker: it is broken'], ['null', null], ['an object', { severity: 'blocker' }]]) {
277
+ await test(`getTaskGrade yields issues: [] when signals.issues is ${label}`, async () => {
278
+ const grade = await db.getTaskGrade(42, { pool: gradeRowPool(storedGradeRow({ issues: malformed })) });
279
+ assert.deepEqual(grade.issues, [], 'a malformed stored value never reaches the top level');
280
+ assert.deepEqual(grade.signals.issues, malformed, 'and the nested key is still left exactly as stored');
281
+ });
282
+ }
283
+
284
+ await test('getTaskGrade returns null when the task has no grade row', async () => {
285
+ assert.equal(await db.getTaskGrade(42, { pool: gradeRowPool(undefined) }), null);
286
+ });
287
+
213
288
  // ======================================================================
214
289
  // Summary
215
290
  // ======================================================================