@bongos/core 1.19.145 → 1.19.146

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.145",
6
- "core_contract": "1.19.145",
7
- "source_commit": "929e362b73dbf98c350bb65eae76dc7f5da408df",
5
+ "core_version": "1.19.146",
6
+ "core_contract": "1.19.146",
7
+ "source_commit": "1363fd9197780c96da219f2f1f211ccdde0d7eae",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-15T16:16:07.290Z",
9
+ "built_at": "2026-08-15T19:30:54.439Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 303,
12
+ "docs_redacted": 304,
13
13
  "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1251,
14
+ "functional_verbatim": 1252,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1575,
20
- "tree_sha256": "60ca1904014209acd11de096311750d88be344765e61259adde3be237818df65",
19
+ "file_count": 1577,
20
+ "tree_sha256": "9c0baec37d19b01ae748459e7d1faaf8308002ba71fa1a6a44da74e0b4617b14",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -1329,10 +1329,15 @@
1329
1329
  "mode": "0000644",
1330
1330
  "sha256": "165324f1fe4fe9a532b5a068a2713bd127d1d9e5926d3964869808ee1af1db47"
1331
1331
  },
1332
+ {
1333
+ "path": "docs/adr/0171-profile-rollup-join-and-consent.md",
1334
+ "mode": "0000644",
1335
+ "sha256": "85e5261934dffea5c36fea620a1cef2c472381ba650d45dcf5f005ab6abf554e"
1336
+ },
1332
1337
  {
1333
1338
  "path": "docs/adr/README.md",
1334
1339
  "mode": "0000644",
1335
- "sha256": "f9bba0779b60b4a9f3014e63d49f4d5518439021f02cbab2e08d02d574edd108"
1340
+ "sha256": "9d16ad8e3b1232cb3d0ff4b1ea8f3ce70a8bab1337780f49a4fc32a24b9a0dc9"
1336
1341
  },
1337
1342
  {
1338
1343
  "path": "docs/api-reference.md",
@@ -4177,12 +4182,12 @@
4177
4182
  {
4178
4183
  "path": "package-lock.json",
4179
4184
  "mode": "0000644",
4180
- "sha256": "17f08bb4a411d05857f562251705ca2d5dd782674d93d6c865a9cd3a8b207e73"
4185
+ "sha256": "92ba433ccabd633bd48e20e6b2d28314ce3c83165e0250ffe943e648bb093571"
4181
4186
  },
4182
4187
  {
4183
4188
  "path": "package.json",
4184
4189
  "mode": "0000644",
4185
- "sha256": "f76658a6a915ebd9c594f48fb4834e90f49991eb1e9053258dc904144b976490"
4190
+ "sha256": "9b6febaf8e44a6a540f3c248efeaf98c41a04ac649cdf609d1eb3ac5ad0f3e64"
4186
4191
  },
4187
4192
  {
4188
4193
  "path": "public-docs/index.html",
@@ -5239,6 +5244,11 @@
5239
5244
  "mode": "0000644",
5240
5245
  "sha256": "a1101fcbb8988a19bd34f5c80789b8d69e421907be4c6143f6052e594c0b6944"
5241
5246
  },
5247
+ {
5248
+ "path": "scripts/gds/seed-bv1-r01-delta-tasks.js",
5249
+ "mode": "0000644",
5250
+ "sha256": "70ab4806b22bd0c8ed5bbea559387af6654f863eccee9a60affda9e7711285b0"
5251
+ },
5242
5252
  {
5243
5253
  "path": "scripts/gds/seed-gds-v4-tasks.js",
5244
5254
  "mode": "0000644",
@@ -5767,7 +5777,7 @@
5767
5777
  {
5768
5778
  "path": "src/module-api.js",
5769
5779
  "mode": "0000644",
5770
- "sha256": "11510c45da266d4f5d22df8268e36b5530d810d927afdf60b43a0dbd94076554"
5780
+ "sha256": "1ee746f8bc9aef75aaf278c5ca67dc6734ceb214a5febc44d6e27556b42dd70a"
5771
5781
  },
5772
5782
  {
5773
5783
  "path": "src/module-loader/catalog.js",
@@ -0,0 +1,46 @@
1
+ # 0171 — Profile ↔ cross-project rollup: the join runs in-process on the hub; consent is fail-closed account state; the hide flag lives on the account, enforced in the read path
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-08-15
5
+ - **Task:** [#1002277](https://cloudbongos.com/builders#/task/1002277) (R01, goal 1000045 — public builder profiles)
6
+ - **Extends:** ADR 0141 (platform identity / hub IdP), [`docs/specs/<redacted>.md`](../specs/<redacted>.md) (spike 1002783), [`docs/specs/<redacted>.md`](../specs/<redacted>.md) (goal spec)
7
+ - **Amends:** profile-page spec Decision 1 (federated instances link out instead of inlining — see Decision 1 below) · ADR 0141's "scouting stays Archon-internal until the T&C lands" condition (superseded by the goal-1000045 owner decision, ratified through P03 — see Consequences)
8
+ - **Related:** ADR 0016 (trust boundary), 0083 (module boundary), 0156 (display-name source), 0157 (`project.curate` floor)
9
+
10
+ ## Context
11
+
12
+ C1/C2 of goal 1000045 want the public builder profile to show a cross-project track record, public by default, with a self-serve hide. The data plane already exists but only on the hub: `platform_identity_builder_project_activity` + `listScoutingRollup` live in the `platform-identity` module (`default: false`, enabled only on cloudbongos.com). A vanilla or standalone instance has none of these tables, and **no instance→hub read path for profile or rollup data exists** — the only instance→hub reads today are the `/sso/token` code redemption and `/sso/pubkey`; everything else is push (`/sso/membership/check-in`, `/sso/activity/rollup`, both fire-and-forget with no timeout or retry, firing only on a federated sign-in).
13
+
14
+ Five facts found during this spike shape the decisions:
15
+
16
+ 1. `platform-identity/module.json` declares `provides: ["platform-identity.hub"]` but the module **never calls `registerProvider`** — the port is declared and unwired. Meanwhile the core profile route (`src/bongos/routes/builders.js`) already models exactly the needed degrade pattern with `seams.resolveOptional('reward')`.
17
+ 2. `listScoutingRollup` (`modules/platform-identity/platform-identity.js:426`) has a correctness bug: `LEFT JOIN LATERAL unnest(a.disciplines) ON true` fans each activity row out **before** `GROUP BY`, multiplying each row's contribution to `project_count` and the credits/ships/karma sums by `max(1, cardinality(disciplines))` (0/1-discipline rows are correct; `max(last_active)` and the disciplines array are unaffected). `/people` and `/scouting` show inflated numbers today — tracked as task 1002911.
18
+ 3. The cross-project rollup is **already visible beyond Archons**: ADR 0157 lowered `project.curate` to a Metic floor, so `/people` and `/scouting` render raw rollup rows to any Metic — while ADR 0141's text still says the view "must stay Archon-internal until [the T&C] lands," and the T&C **does not exist in the tree**. Any hide/consent design that ignores those two live surfaces is fiction.
19
+ 4. The consent premise of "public by default" is false for a subset of accounts: `POST /sso/membership/check-in` and `POST /sso/device/poll` upsert `platform_identity_accounts` rows for builders who never saw a hub page (the device flow deliberately mints no hub session and runs no hub admission). Whether those accounts get public profiles is the pending owner decision P03 (task 1002799). `POST /sso/activity/rollup` creates **no** account row at all, so orphan activity rows exist.
20
+ 5. Nothing about privacy exists yet — no hide flag, no private-account column. The account-level privacy *model* (private accounts, recruiter discoverability, invite lifecycle) belongs to spike 1002785 and is **not** decided here; this ADR decides only the C2 hide-stats flag and the consent state, and stays inside that boundary.
21
+
22
+ ## Decision
23
+
24
+ **1. The profile↔rollup join runs in-process on the hub, through the already-declared `platform-identity.hub` port.** The module registers a real provider (hasProvider-guarded, in its route factory, like `builder-settings`); the core profile route adds `seams.resolveOptional('platform-identity.hub')` beside the existing `reward` resolve. On the hub, `GET /builders/:id/profile` gains a `cross_project` section with zero network calls and zero new auth surface; on every other instance the port resolves `null` and the section is absent — the "degrade, don't 500" rule for free. Rejected: **(a)** instance→hub request-time pull — extends ADR 0141's "always-on hub dependency" from sign-in to every profile view and needs a net-new server-to-server read endpoint with no timeout/retry plumbing to copy; **(b)** hub→instance rollup mirroring — no plumbing exists in that direction, and any instance-side copy is stale the moment a builder hides (breaking C2's "live everywhere"). That staleness argument is also why this ADR **amends profile-page spec Decision 1**: federated non-hub instances in V1 **link out instead of inlining numbers** — `rel=canonical` plus a visible "see the cross-project record" link to the hub profile. The cost is named plainly: C1's cross-project render is hub-only in V1. Task 1002804 (P08) now owns the federated render path (link, canonical emission, numeric-id 301). If the owner later wants inline numbers on federated instances, the named follow-up is a pull-with-cache client modeled on `project-stats.js` against a new client-authenticated hub read — a deliberate new task, not scope creep on R02.
25
+
26
+ **2. One safe read path, enforced next to the data.** The port exposes `getPublicProfileExtras(githubId)`: `null` unless the account exists, is publicly addressable (Decision 3), and `hide_stats` is false — otherwise `{ totals: { projects, credits, works_shipped, karma }, disciplines, last_active, projects: [{ name, origin, rank_held }...], more_projects }`, whitelist-shaped **inside the module** (`client_id` and raw rows never cross the port; non-numeric `github_id` values like the `system:*` principals return `null` instead of hitting a bigint cast error). Every surface that shows cross-project numbers reads through the module's filtered functions at render time, never from a stored copy — and that includes the two that already exist: **`listScoutingRollup` itself gains the `profile_state = 'active' AND NOT hide_stats` predicate and a whitelisted projection**, so `/people` and `/scouting` (live today at Metic floor) honor hide by construction, not by memory. The hub hall profile, the `/u/<handle>` page, recruiting (R14/R15) and applications (R09) all sit on the same functions, which is what makes C2's "honored live everywhere, including pending applications" structural. Two deliberate side doors, both narrow: `getOwnProfileExtras(githubId)` serves the builder their own numbers regardless of flags (Settings), and spike 1002785's recruiter flow is expected to add a second **authorization-scoped** read for its opt-in discoverability sliver — `hide_stats` is the invariant every reader honors; public addressability is not.
27
+
28
+ **3. Consent is a fail-closed `profile_state` on `platform_identity_accounts`, with one discriminator.** Migration `<redacted>.sql` (lands with R02) adds `profile_state text NOT NULL DEFAULT 'provisional' CHECK (profile_state IN ('provisional','active'))` and `hide_stats boolean NOT NULL DEFAULT false`. The single rule: **an account is `active` iff it was touched by a hub-session `/sso/authorize`** — the one path where a human is demonstrably at the hub (and where T&C acceptance can later be captured, task 1002913). Device-poll and check-in create `provisional`; the rollup endpoint creates nothing (orphan activity rows are never publicly addressable). Backfill: **all existing accounts start `provisional`** and lazily upgrade on their next hub-session authorize — no join to core `builders` (that join is type-unsafe against `system:*` text ids and would put a core-table read inside a module migration; the price is that existing hub builders' profiles publish on their next sign-in rather than immediately). Public surfaces read only `active` accounts. **R02 ships this fail-closed default without waiting on P03**, because the asymmetry only runs one way: P03 can then only ever *widen* exposure, never claw back something already published. This deliberately narrows the Phase-4 interview's "no extra consent gate" to the accounts whose consent premise actually holds; P03 is the owner either ratifying that boundary (recommended) or reverting to the interview decision verbatim — and the revert branch has a named precondition: check-in/device-created identities have **no self-serve hide path** (no hub builder session exists for them; hub admission is invite-gated), so rejecting P03 first requires an assertion-authenticated hide path or open hub enrollment. Task 1002799 carries this full decision payload.
29
+
30
+ **4. The C2 hide flag is account-global and lives on `platform_identity_accounts.hide_stats` — not in `behavior_prefs`.** Three reasons: the data it guards is account-global hub data, so a per-instance jsonb key would fork the flag per instance; the public profile route must not grow a core→`builder-settings` read dependency that doesn't exist today; and enforcement belongs in the module's read functions (Decision 2), next to the data, where no caller can forget it. (C2's "(builder-settings)" parenthetical is therefore one of the criterion texts task 1002914 amends — the control is still self-serve in hall Settings; only its storage moves.) The write path is an own-scoped `PATCH` route in `platform-identity` (keyed by `req.builder.github_id`, `validateOrRespond`-validated so it needs no `WRITE_VALIDATION_EXEMPT` entry). Public profile responses stay **viewer-independent** — the surviving justification, with edge caching gone (Decision 5), is that a single response shape is the only thing the mirror leak test can actually pin — so the profile page renders the self-toggle client-side by comparing the already-available `/me` identity, and a builder whose stats are hidden inspects their own numbers in Settings via the own-scoped read (the C1 amendment in task 1002914 confirms Settings as the self-view home). Hidden → the track-record section renders the "keeps their track record private" line and **nothing** derived from the rollup ("no partial numbers; a count plus a hidden total is a leak by subtraction"); the local-instance profile keeps its local data (C2's fallback clause applies to the instance render path — the hub has no local data to fall back to). One residual stated honestly: hide removes the hub's *aggregated* view, not the per-instance public numbers it is derived from — every instance's `/public/leaderboard` stays public by design, so a determined reader can still reconstruct a sum the hard way. The C2 amendment text carries that caveat.
31
+
32
+ **5. Hide must beat the cache: when the port resolves (hub), profile responses are `Cache-Control: no-store`.** The route's current `public, max-age=60` would keep serving a builder's stats for up to 60s after they hide — failing C2's "takes effect immediately" and the R06 proof as literally specified (a browser-cached copy survives a reload). On plain instances the existing `max-age=60` stays. The server-rendered `/u` page inherits the same rule for hide-sensitive content (its caching posture is otherwise goal 1000048's call). Cost: hub profile API responses aren't edge-cached — acceptable at current scale (the rollup read is one indexed aggregate per view).
33
+
34
+ **6. Fix the rollup aggregation before any reuse.** `getRollupForBuilder(github_id)` (R02) is built on a corrected aggregation — scalar sums over un-fanned rows, the disciplines union via a separate `array_agg(DISTINCT …)` — and `listScoutingRollup` is fixed to the same shape in task 1002911 (the inflation is live today, independent of this goal). Testing is stated honestly: module tests run in the DB-free gate and the existing test only regex-matches SQL text, so the fix either extracts the aggregation into an assertable shape or brings real-SQL coverage (pg-mem / integration DSN) with it — task 1002911's body carries both options.
35
+
36
+ **7. What is public is exactly C1's set, nothing more — and it must be hardened before it is published.** Totals (project count, credits, works shipped, karma), the disciplines union, `last_active`, and per-project rows carrying `name`, `origin`, and `rank_held` labelled as *that project's* standing, as reported by that project ("Metic on Foo") — never any platform-level rank (profile-page spec Decision 1's hard constraint; ADR 0016: descriptive analytics, never authority). Project names appear only where the catalog row is public; non-public projects collapse into `more_projects` — counted, never named ("the aggregate is not the leak, the name is"; the catalog join is the module-internal `client_id → sso_clients.origin → projects.origin` two-hop). But these rows are third-party self-report, and today `/sso/activity/rollup` accepts them for **any** `github_id` from any registered client, with `rank` and `disciplines` effectively unvalidated — tolerable for an internal tool, an impersonation surface on a public portfolio. So R02's brief includes, before anything publishes: require an existing membership row for `(client_id, github_id)`; validate `rank` against the four core rank keys (else null); cap `disciplines` to string elements, ≤8 of them, ≤32 chars each; and sequence check-in **before** rollup in `src/bongos/auth.js` so the membership requirement cannot race a first sign-in.
37
+
38
+ **8. The addressing model from profile-page spec Decision 3 is ratified, and R05/P08 get disjoint scopes.** `handle` on `platform_identity_accounts` (P01), canonical `/u/<handle>` on the hub, `github_id` stays the join key everywhere. Task 1002281 (R05) is retargeted off its stale `?builder=<login>` brief to **handle-based lookup** (handle → account resolution incl. the previous-handle 301 window, feeding the page that goal 1000048 R14 / task 1002365 builds — exactly one page); task 1002804 (P08) keeps canonical wiring (instance `rel=canonical`, numeric-id 301) plus the federated link-out from Decision 1.
39
+
40
+ ## Consequences
41
+
42
+ - **Sub-tasks are live, not promised.** Created by this spike (seeder `scripts/gds/seed-bv1-r01-delta-tasks.js`, idempotent): task 1002911 (rollup aggregation bug — ready), task 1002912 (reserve `u` as a top-level project slug so a project can't shadow the profile namespace — ready; `u` is missing from goal 1000048's reserved list), task 1002913 (platform T&C page + acceptance capture, dep P03), task 1002914 (apply the C1/C2 criterion text amendments, Archon, dep P03). Amendment sections were appended to tasks 1002278 (R02 build brief), 1002279 (R03 residual: the write path; now also depends on R02), 1002280 (R04), 1002281 (R05 retarget), 1002282 (R06 proof additions), 1002798 (P02 + the `upsertAccount` display-name clobber guard, ADR 0156 pattern), 1002799 (P03 decision payload), 1002803 (P07 + ship-time rollup push + check-in→rollup sequencing), 1002804 (P08 + federated link-out). Dependency edges made real: R03 → {R01, R02}; P09 → {P01, 1002785, **P03**} (the spec had that last edge in prose only).
43
+ - **Criterion texts are amended through tasks, not unilaterally.** C1's existing route/leak-test references are *correct* for this design (the `cross_project` section lands on exactly that route, guarded by that test) — the amendment **adds** the hub surface + P05's mirror test alongside them, resolves the "public by default for any builder with a platform account" phrase per P03, and fixes C2's storage parenthetical, fallback clause, and the reconstruction residual. P03 (task 1002799) carries the decision payload; task 1002914 (Archon) applies the texts on the scope-protected goal.
44
+ - **ADR 0141's condition is superseded, on the record.** The goal-1000045 owner decision (public by default with self-serve hide) supersedes 0141's "stay Archon-internal until the T&C lands" — this ADR is where that's written down, P03 is where the owner ratifies the boundary for consent-less accounts, and task 1002913 finally builds the T&C artifact the original condition assumed. Until R02 lands, note the standing drift: ADR 0157 already opened the raw rollup to Metics while 0141's text still says Archon-only; Decision 2's predicate narrows what those surfaces show going forward (hidden + provisional builders drop out of `/people` and `/scouting`).
45
+ - **`tests/profile_route.mjs` grows a second scope.** Its key-level assertions cover only `r.json.builder`; the new `cross_project` object needs its own PUBLIC/PRIVATE key lists plus sentinel strings planted on the port-stub fixture (`client_id`, a fake private project name) so the raw-text scan bites. The hub mirror test is P05, unchanged.
46
+ - **What this ADR deliberately does not decide:** the private-account model, recruiter discoverability and its pre-accept metadata boundary, and the invite lifecycle (spike 1002785 — whose recruiter read will be a separate authorization-scoped function, per Decision 2); the application record's storage shape (spike R07); guild/connection models (spikes R18/R23). If 1002785 lands a different privacy rule, its model wins and the page-behaviour notes here follow it.
@@ -243,6 +243,7 @@ This keeps the decision history honest and traceable.
243
243
  | 0168 | [**The scaffold vendors the hub's RUNNING core (`npm pack` of the installed package); registry-default deferred to the go-public step** ([task 1002706](https://cloudbongos.com/builders#/task/1002706); gap 13 of the project-creation audit). Two collided facts: children were born pinned to a frozen `file:vendor/bongos-core-1.19.2.tgz` (installedCoreTarball reuses the hub's own file: pin — task 1002275 — and the hub's pin sat at 1.19.2), and the task-1002621 registry cutover (hub pin now `"@bongos/core": "1.19.56"`, verified on the box) made installedCoreTarball return null — dropping the scaffold into the package-core.js source rebuild that ABORTS on a consumer instance (empty core set → empty child repo, the 1002275 failure resurrected). Fix: a three-rung ladder — file:-pin reuse → NEW `installedCorePackDir()` + `npm pack` of the hub's installed core (child pins the SAME, CURRENT core, stays file:-pinned, owner needs no token) → source rebuild. The brief's "make registry the default now that the package is public" was verified FALSE (both npm scopes 404 anonymously; ADR 0134 keeps them private until go-public) — a registry-default child would be born UNINSTALLABLE for a tokenless owner. Registry-default is deferred to the go-public step (idea 1000700); the gap-13 "flip the box env" ops leg is explicitly a no-op until then; child freshness rides this ladder + the hub-pin lane (task 1002708). Rejected: registry-default now, dropping NPM_TOKEN, vendoring from the npm cacache.](<redacted>.md) | provisioning / distribution |
244
244
  | 0167 | [**The /create gate trust link defaults to the AGPL-3.0 license text, not the npm package page** ([task 1002705](https://cloudbongos.com/builders#/task/1002705); gap 1 of the project-creation audit). The gate's "open source" link pointed at a nonexistent repo (404); the task brief's suggested default — the npm package page — is ALSO dead for the gate's actual audience (signed-out visitors): `@cloudbongos/core` and `@bongos/core` both 404 anonymously (private scopes, ADR 0134), and the ADR 0099 public mirror stays dormant until ~2026-11. The license text is the one live, instance-neutral artifact behind the claim today. The browsable-code upgrade is the owner's filed choice (idea 1000700): public repo now (forfeits the ADR 0065 §4 redaction window) / public npm package / wait for the mirror. Rejected: the npm link (verified dead), flipping the repo public (not a builder's call), plain unlinked text (less information, same cost).](<redacted>.md) | public landing / governance |
245
245
  | 0169 | [**`bongos serve`: the `bongos dev` boot path, on a real host** ([task 1002398](https://cloudbongos.com/builders#/task/1002398), goal 1000052). [ADR 0149](<redacted>.md) gave an owner one command to stand an instance up on their laptop; "now put it on a server" had no equally good answer — Docker serves the *vanilla* brand on `:3000`, `bongos onboard` provisions a whole droplet through the control plane, and [`instance-deploy.md`](../recipes/instance-deploy.md) left the systemd unit as a hand-written prose step. The six steps (deps → Postgres → createdb → migrate → env → server → health) were already right for a host; only four *decisions inside them* were laptop-shaped. So: a `--server` **mode** on the same script (`bongos serve`), never a second boot path, so the two can't drift. (1) **Supervision** — a systemd unit (`Restart=always`, `WantedBy=multi-user.target`) instead of a pid file that dies on reboot, falling back to the pid-managed process where systemd is absent rather than refusing. (2) **Bind address, derived from the origin** — `https://…` means a proxy terminates TLS, so bind `127.0.0.1`; plain `http://` on a public host means nothing is in front, so bind `0.0.0.0`. The common misconfiguration (app publicly reachable on its raw port behind a proxy) becomes impossible by default. (3) **Peer-auth bootstrap** — a fresh `apt-get install postgresql` cluster has only the `postgres` role, so a deploy user's `createdb` fails with `role "deploy" does not exist`; that reads like a broken install and is really an unbootstrapped cluster, so we create the login role as the superuser. (4) **Health means the PUBLIC origin** — the loopback probe proves the app; DNS/proxy/TLS are where a host actually fails, so it probes the advertised origin too and names the missing layer (advisory — DNS may legitimately not be wired). Secrets go to a root-owned `0600` `EnvironmentFile`, never `Environment=` (`systemctl show` leaks those, and the laptop path's `buildEnv()` spreads the caller's whole shell). Every decision is a pure injectable in `dev-lib.js`, proved in `tests/dev_lib.mjs` with no host. Rejected: a separate `deploy.sh` (drift — the point is the *same* six steps), Docker-on-the-host (makes brand+port a compose override), always binding `0.0.0.0` (exposes the raw port), refusing without systemd (breaks containers/BSD).](<redacted>.md) | onboarding / deploy |
246
+ | 0171 | [**Profile ↔ cross-project rollup: the join runs in-process on the hub; consent is fail-closed account state; the hide flag lives on the account, enforced in the read path** ([task 1002277](https://cloudbongos.com/builders#/task/1002277), R01 of goal 1000045). The C1 cross-project profile reads the platform-identity rollup through the module's **declared-but-never-registered `platform-identity.hub` port**: the module registers a real provider and core `GET /builders/:id/profile` `resolveOptional`s it beside `reward` — zero network, `null` (clean degrade) on every non-hub instance. Federated instances **link out** instead of inlining (amends profile-page spec Decision 1: an instance-side copy is either stale-on-hide or a per-view hub call). Consent: `profile_state` on `platform_identity_accounts`, **fail-closed** — `active` iff touched by a hub-session `/sso/authorize` (device-poll/check-in create `provisional`; backfill = all provisional, lazy upgrade; no core-`builders` join — type-unsafe vs `system:*` ids and a module-migration boundary breach), so the P03 owner decision can only ever widen, never claw back. The C2 hide flag is `hide_stats` on the account (NOT per-instance `behavior_prefs`), enforced **inside** the module's read functions — `listScoutingRollup` itself gains the `active AND NOT hide_stats` predicate so the live Metic-floor `/people`+`/scouting` surfaces honor it by construction. Hub profile responses go `no-store` (hide must beat the 60s cache). Publishing is gated on hardening the self-reported rollup (membership-row requirement, rank-vocab validation, disciplines caps, check-in→rollup sequencing). Found + filed: the `LATERAL unnest` fan-out inflating rollup sums (task 1002911), the unreserved `u` slug (task 1002912), the missing platform T&C artifact (task 1002913), criterion amendments (task 1002914). Rejected: instance→hub per-view pull (always-on dependency), hub→instance mirroring (stale-on-hide), storing the flag in builder-settings, backfilling `active` from hub `builders` rows.](<redacted>.md) | platform identity / profiles / privacy |
246
247
  | 0170 | [**The featured projection tells time honestly: `created_at` in, `updated_at` out, owner from the project's own archon** ([task 1002778](https://cloudbongos.com/builders#/task/1002778), goal 1000061). The hub card's metadata pass added `created_at` to the public `/projects/featured` projection, labelled **Listed** — it is the moment the hub catalogued the row, never when the project began, and the C6 card test bans the word "Created". Two recorded deviations: (1) the task brief said `updated_at` was ready to add, but the column is bumped by every client re-registration AND the Archon's feature toggle, so as a public "last updated" it would report curation noise as project activity — it may sort, never project, and the test pins the SELECT list itself (`SELECT *` banned: the route is CORS-open rank:public and a star would leak `owner_github_id`/`status`). (2) The owner fact reads the project's OWN leaderboard archon row (login + avatar, type-checked and capped) from the payload project-stats already fetches — zero extra requests, the 2-fetch pin intact, the advisory `owner_github_id` column still unread per the design contract. Multi-archon: top-credits archon is named; a fallen-off-the-page archon renders as no owner shown, never a wrong one. Rejected: projecting `updated_at` as asked (it lies), reading `owner_github_id` (advisory, contract-banned), a third per-origin fetch for activity recency (moves the test pin for a fact nobody asked for yet).](<redacted>.md) | platform identity / public surface |
247
248
 
248
249
  > ⚠️ **Numbering collision (known):** two ADRs share `0008`. The numbers were assigned in parallel sessions before the lock could be enforced. The files are kept under their existing filenames to preserve git history, but the table disambiguates them as `0008-a` / `0008-b`. New ADRs should resume at `0040` to avoid further collisions. (`0024` is also a real on-disk duplicate: the cloneable-repo/local-first-memory ADR and the multi-agent-system-architecture ADR were both numbered `0024` in parallel; both keep their filenames, disambiguated as `0024-a` / `0024-b` like `0008`. Likewise `0025` ended up with two on-disk files — the offsite-backup ADR and the structured-criterion↔task-link ADR — both kept under their existing filenames and disambiguated as `0025-a` / `0025-b`; separately, the BFG ADR had also claimed `0025` on 2026-05-30 and yielded to `0026`. A third happened 2026-06-06: `0036` was claimed in parallel by the builders-subdomain ADR ([#726](https://example.com/builders#/task/726)) and this Discord-channels ADR ([#727](https://example.com/builders#/task/727)); the Discord-channels ADR yielded to `0037`. A fourth landed 2026-06-10: `0044` was claimed in parallel by the palette-replacement ADR and the per-box game-preview ADR ([#897](https://example.com/builders#/task/897)); both keep their filenames, disambiguated as `0044-a` / `0044-b` like `0008`. A fifth was caught pre-land 2026-06-13: `0050` was claimed in parallel by the device-flow-admission ADR ([#1000](https://example.com/builders#/task/1000)) and the sandbox-for-everyone ADR ([#1001](https://example.com/builders#/task/1001)); the latter yielded to `0052` before merging, so there is no `0050` duplicate. A sixth was caught pre-land 2026-06-21: the tool-agnostic-design-layer ADR ([#1375](https://example.com/builders#/task/1375)) first collided at `0079` with the value-gated-autonomous-engagement ADR, then at `0080` with the llm-cache-api-transport ADR (both part of the 0075–0080 LLM-cost-reduction series, landed on `main` in parallel); it yielded to `0081` before merging, so there is no `0079`/`0080` design-layer duplicate. A seventh landed 2026-06-23: `0084` was claimed in parallel by the auto-derived-per-task-rank-floor ADR and the remove-default-spend-cap ADR (task 1499); the rank-floor ADR landed on `main` first, so the spend-cap ADR yielded to `0085`, leaving no `0084` duplicate. An eighth landed 2026-06-27: `0095` was claimed in parallel by the borrowed-memory-and-retrieval-concepts ADR (PR #459, landed first) and the cross-agent-context-management ADR ([#1651](https://example.com/builders#/task/1651), PR #460); both keep their filenames, disambiguated as `0095-a` / `0095-b` like `0008`. These two are companions — `0095-a` adopts the memory/retrieval concepts and explicitly defers the grep-vs-semantic question to `0095-b`.)
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.145",
3
+ "version": "1.19.146",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.145",
9
+ "version": "1.19.146",
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.145",
3
+ "version": "1.19.146",
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",
@@ -0,0 +1,154 @@
1
+ // Seed + amend the delta the R01 spike (task 1002277, ADR 0171) surfaced, under
2
+ // goal 1000045 "Public builder profiles" (BONGOS-V1, internal).
3
+ //
4
+ // Source of truth: docs/adr/0171-profile-rollup-join-and-consent.md.
5
+ // Three things happen here, all idempotent / convergent on re-run:
6
+ // 1. Four NEW tasks (idempotent on source_ref bv1-r01-delta-D0N).
7
+ // 2. Dependency rewires ADR 0171 decides (R03 gains R02; P09 gains P03).
8
+ // 3. Amendment sections APPENDED to affected task bodies (guarded by a
9
+ // marker string so a re-run never double-appends).
10
+ //
11
+ // Run: node scripts/gds/seed-bv1-r01-delta-tasks.js
12
+
13
+ const { apiCall } = require('./cli-lib');
14
+
15
+ const VER = 'BONGOS-V1';
16
+ const GOAL = 1000045;
17
+ const SRC = 'planning-session';
18
+ const ADR = 'docs/adr/0171-profile-rollup-join-and-consent.md';
19
+ const MARK = '— ADR 0171 amendments (R01, 2026-08-15) —';
20
+
21
+ // Existing tasks referenced by id (seeded by seed-bv1-profiles-tasks.js and
22
+ // seed-bv1-profile-page-tasks.js).
23
+ const R01 = 1002277, R02 = 1002278, R03 = 1002279, R04 = 1002280, R05 = 1002281,
24
+ R06 = 1002282, P02 = 1002798, P03 = 1002799, P07 = 1002803, P08 = 1002804,
25
+ P09 = 1002805, P01_HANDLE = 1002797, PRIVACY_SPIKE = 1002785;
26
+
27
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
28
+
29
+ // Throttled + 429-aware wrapper (same shape as the sibling seeders).
30
+ async function api(method, path, body, _tries = 0) {
31
+ await sleep(1500);
32
+ const r = await apiCall(method, `/api/gds${path}`, body);
33
+ if (r.status === 429 && _tries < 8) {
34
+ const wait = (r.data?.error?.details?.retry_after_seconds || 30) + 1;
35
+ console.log(` …rate-limited, waiting ${wait}s`);
36
+ await sleep(wait * 1000);
37
+ return api(method, path, body, _tries + 1);
38
+ }
39
+ return r;
40
+ }
41
+
42
+ const NEW_TASKS = [
43
+ {
44
+ ref: 'D01', kind: 'bug', prio: 4, est: 60, crit: [], deps: [], ready: true,
45
+ title: 'listScoutingRollup inflates totals: LATERAL unnest fans rows out before GROUP BY',
46
+ desc: `modules/platform-identity/platform-identity.js:426-445 — \`LEFT JOIN LATERAL unnest(a.disciplines) AS d ON true\` multiplies each activity row by max(1, cardinality(disciplines)) BEFORE aggregation, so project_count and the credits/ships/karma sums are inflated for any builder with 2+ disciplines on a project row (0/1-discipline rows are correct; max(last_active) and the disciplines array are unaffected — do not "fix" those). Live today on /people and /scouting. Fix: aggregate scalars over un-fanned rows; compute the disciplines union in a separate array_agg(DISTINCT …) over a pre-aggregated CTE/subquery. Testing, honestly: module tests are DB-free (scripts/gds/run-unit-tests.js gate) and the existing test only regex-matches SQL text — either extract the aggregation into an assertable SQL-builder shape, or add real-SQL coverage (pg-mem devDep or an integration DSN) as part of this task. getRollupForBuilder (task ${R02}) builds on the corrected shape. Found by R01; see ${ADR}.`,
47
+ },
48
+ {
49
+ ref: 'D02', kind: 'feature', prio: 4, est: 30, crit: [], deps: [], ready: true,
50
+ title: 'Reserve `u` as a top-level slug so project slugs cannot shadow builder profiles',
51
+ desc: `docs/specs/bongos-v1-seo-public-discoverability.md:19 reserves bongos/api/sso/gate/create/explore/builders/status/assets/.well-known/sitemap.xml/robots.txt/favicon.ico as top-level project slugs — but NOT \`u\`. A project could claim slug \`u\` and shadow the entire canonical builder-profile namespace /u/<handle> (profile-page spec Decision 1; ${ADR} D8). Add \`u\` to the reserved list in that spec AND wherever slug validation enforces it (coordinate with goal 1000048). While there, flag the future guild namespace for the same treatment in spike R23 (task 1002299) — reserve, don't squat.`,
52
+ },
53
+ {
54
+ ref: 'D03', kind: 'feature', prio: 3, est: 120, crit: [], deps: [P03],
55
+ title: 'Platform T&C page + acceptance capture at hub sign-in (the consent artifact ADR 0141 assumes)',
56
+ desc: `ADR 0141 defers cross-project-analytics consent to "the platform T&C" and conditioned un-gating the scouting rollup on it — but no T&C document, route, page, or acceptance record exists anywhere in the tree (the only reference is a comment at modules/platform-identity/routes/scouting.js:6). Goal 1000045 now makes the same data public by default (owner decision), which makes the artifact overdue: create the T&C page on the hub, record acceptance (timestamp on the platform account) at hub-session sign-in, and tie profile_state 'active' semantics to it once it exists (${ADR} D3). The T&C wording itself is an owner input — draft it, then file a blocker if the owner wants different wording. Depends on P03 (task ${P03}) so the consent boundary is decided before the artifact bakes it in.`,
57
+ },
58
+ {
59
+ ref: 'D04', kind: 'cleanup', prio: 4, est: 20, crit: ['C1', 'C2'], deps: [P03], rank: 'archon',
60
+ title: 'Amend C1/C2 criterion texts per ADR 0171 + the P03 outcome',
61
+ desc: `Apply the criterion amendments ${ADR} enumerates (decision payload carried in task ${P03}'s body): C1 — resolve "public by default for any builder with a platform account" per P03's outcome, and ADD the hub surface + its P05 mirror leak test ALONGSIDE the existing route/test references (those are correct and stay — the cross_project section lands on exactly GET /builders/:id/profile, guarded by tests/profile_route.mjs). C2 — "(builder-settings)" becomes "self-serve in hall Settings, stored account-global on the platform account"; "falls back to local-project data" is scoped to the instance render path (the hub has no local data); add the residual note that hide removes the hub AGGREGATE, not the per-instance public numbers it is derived from. Archon: the criteria live on scope-protected goal 1000045.`,
62
+ },
63
+ ];
64
+
65
+ // Body amendments appended to existing tasks. Each entry: [id, text].
66
+ const AMEND = [
67
+ [R02, `Build brief from ${ADR} (decisions D1-D7):
68
+ (1) Migration platform_identity_007_profile_consent.sql: profile_state text NOT NULL DEFAULT 'provisional' CHECK (IN 'provisional','active') + hide_stats boolean NOT NULL DEFAULT false, on platform_identity_accounts. NO backfill joining core builders (type-unsafe: 'system:bfg'-style text ids vs bigint; boundary-violating: module migration reading core state). All existing accounts start provisional; a hub-session /sso/authorize lazily upgrades to active. Device-poll and check-in create provisional; /sso/activity/rollup creates NO account (orphan activity rows are never publicly addressable).
69
+ (2) getRollupForBuilder(github_id) + the memberships-to-projects join (client_id -> sso_clients.origin -> projects.origin) with CORRECTED aggregation — no LATERAL-unnest fan-out (bug task {{D01}}).
70
+ (3) listScoutingRollup gains the profile_state='active' AND NOT hide_stats predicate + a whitelisted projection: /people and /scouting must honor hide BY CONSTRUCTION (they are live cross-project surfaces at Metic floor today).
71
+ (4) Register the declared-but-unwired 'platform-identity.hub' port (hasProvider-guarded, route-factory pattern; module.json already declares provides). Port: getPublicProfileExtras(githubId) -> null unless active AND not hidden, else {totals:{projects,credits,works_shipped,karma}, disciplines, last_active, projects:[{name,origin,rank_held}], more_projects} — whitelist-shaped INSIDE the module (client_id never crosses); guard non-numeric github_id -> null (never a bigint cast error). getOwnProfileExtras(githubId) serves the self view regardless of flags.
72
+ (5) Core GET /builders/:id/profile resolves the port beside 'reward', attaches cross_project; Cache-Control: no-store when the port resolves (hide must beat the 60s cache), unchanged max-age=60 elsewhere. Project names only where the catalog row is public; others counted in more_projects, never named.
73
+ (6) Harden POST /sso/activity/rollup BEFORE anything publishes: require an existing membership row for (client_id, github_id); validate rank against the four core rank keys (else null); disciplines: string elements only, cap count (<=8) and element length (<=32). Sequence check-in BEFORE rollup in src/bongos/auth.js (await the check-in) so the membership requirement cannot race a first sign-in.
74
+ (7) Extend tests/profile_route.mjs: a second PUBLIC/PRIVATE key list scoped to cross_project + sentinel strings planted on the port-stub fixture (a client_id, a fake private project name) so the raw-text scan bites.`],
75
+ [R03, `The hide_stats column and the read-path enforcement land with R02 (task ${R02} — now a declared dependency). R03's residual scope: the WRITE path — an own-scoped PATCH route in platform-identity keyed by req.builder.github_id (validateOrRespond-validated, '// rank: any-builder'), the hall Settings toggle (P04's Profile rail group) and the profile-page self-toggle handoff to R04, plus verification that /people, /scouting, and the profile all drop the numbers live on toggle. Public responses stay viewer-independent; a builder inspects their own hidden stats via the own-scoped read (getOwnProfileExtras), not the public route. See ${ADR} D4.`],
76
+ [R04, `Per ${ADR}: the self hide-toggle renders CLIENT-SIDE only, after comparing the /me identity to the profile id — the profile response stays viewer-independent (single response shape keeps the leak test honest). Hidden -> render the "keeps their track record private" line and NOTHING derived from the rollup (no partial numbers — a count plus a hidden total is a leak by subtraction). Keep the instance rank badge (the platform-rank ban applies to the hub identity band only — profile-page spec Decision 1). Reserve layout slots for connections/guilds/actions (spec Decision 5). Hub profile API responses are no-store (ADR 0171 D5) — do not build UI caching assumptions on them.`],
77
+ [R05, `Retarget per ${ADR} D8 — the ?builder=<login> brief is superseded (github_login is mutable; profile-page spec Decision 3). R05 now delivers HANDLE-BASED LOOKUP: resolve <handle> -> platform account (case rules + the previous-handle 301 window from P01, task ${P01_HANDLE}) and the profile read the hub's canonical page consumes. The page itself is goal 1000048 R14 (task 1002365) — coordinate; exactly one page gets built. The numeric-id 301 and instance-side rel=canonical stay with P08 (task ${P08}).`],
78
+ [R06, `Proof additions from ${ADR}: assert the hide toggle takes effect on immediate reload (no-store — the old public,max-age=60 would fail this); assert /people and /scouting also drop the hidden builder (the read-path enforcement, not just the profile); assert the extended leak test (cross_project key lists + sentinels) goes red when a private field is planted. State the residual in the proof notes: hide removes the hub's AGGREGATED view — per-instance public leaderboards remain public by design.`],
79
+ [P02, `Addition from ${ADR}: upsertAccount (modules/platform-identity/platform-identity.js:104-117) overwrites display_name/avatar_url unconditionally on every check-in — a hub-edited display_name would be silently clobbered by the builder's next federated sign-in anywhere. Mirror the ADR 0156 source-tracked guard (user-set wins) for the hub account fields as part of this task.`],
80
+ [P03, `Reframe from ${ADR} (read before deciding): (1) The fail-closed mechanics ship with R02 regardless of this decision: profile_state DEFAULT 'provisional'; ONLY a hub-session /sso/authorize upgrades to 'active' (the SSO device flow never shows a hub page and mints no hub session — it creates provisional; so does check-in). Backfill: ALL existing accounts start provisional and lazily upgrade on their next hub-session authorize. So this decision can only ever WIDEN exposure — nothing needs clawing back. (2) ACCEPT = ratify that boundary (recommended; it narrows the Phase-4 interview's "no extra consent gate" to accounts whose consent premise actually holds). REJECT (= all accounts public, the interview decision verbatim) has a hard precondition: check-in/device-created identities have NO self-serve hide path today (no hub builder session exists for them; hub admission is invite-gated) — rejecting requires first building an assertion-authenticated hide path or opening hub enrollment. (3) The C1/C2 criterion text amendments ride task {{D04}} (dep on this one); the replacement texts are enumerated there and in the ADR. (4) The T&C artifact rides task {{D03}} (dep on this one).`],
81
+ [P07, `Addition from ${ADR}: the rollup fires only on federated sign-in today, so hub numbers can be months stale with no indicator. Add a ship-time push (lifecycle ship path -> reportActivityRollup) alongside the sign-in trigger, and SEQUENCE check-in before rollup (await the check-in) so the membership-row requirement R02 adds to /sso/activity/rollup cannot race a first sign-in.`],
82
+ [P08, `Amendment from ${ADR} D1 (this AMENDS profile-page spec Decision 1): federated non-hub instances LINK OUT instead of inlining a cross-project section in V1 — an instance-side copy is either stale-on-hide (breaks C2's "live everywhere") or a per-view hub call (rejected: always-on dependency); the canonical page is one click away. P08 now owns the federated render path: the visible "see the cross-project record" link + rel=canonical emission (needs a server-side hub-origin stamp token — clientBranding()/serve-internal.js carry no hub origin today, and goal 1000048 R13 flags this Metic+) + the numeric-id 301. Coordinate with goal 1000048 R13/R14.`],
83
+ ];
84
+
85
+ (async () => {
86
+ const idByRef = {};
87
+ let created = 0, existing = 0;
88
+
89
+ console.log(`Seeding ${NEW_TASKS.length} delta tasks under goal ${GOAL} (${VER})…`);
90
+ for (const t of NEW_TASKS) {
91
+ const r = await api('POST', '/tasks', {
92
+ version_id: VER,
93
+ goal_id: GOAL,
94
+ title: t.title,
95
+ description: t.desc,
96
+ kind: t.kind,
97
+ discipline: 'engineer',
98
+ priority: t.prio,
99
+ est_minutes: t.est,
100
+ criterion_ids: t.crit,
101
+ source: SRC,
102
+ source_ref: `bv1-r01-delta-${t.ref}`,
103
+ });
104
+ if (!r.ok) { console.log(` ${t.ref}: FAILED ${r.status} ${JSON.stringify(r.data)}`); continue; }
105
+ const task = r.data.task || r.data;
106
+ idByRef[t.ref] = Number(task.id);
107
+ if (r.data.idempotent || r.data.existing) existing++; else created++;
108
+ console.log(` ${t.ref} → task ${task.id} (${t.kind}, P${t.prio})`);
109
+ }
110
+
111
+ console.log('Wiring dependencies…');
112
+ const DEP_SETS = [
113
+ ...NEW_TASKS.filter((t) => t.deps.length).map((t) => [idByRef[t.ref], t.deps]),
114
+ [R03, [R01, R02]], // ADR 0171: column+enforcement land in R02
115
+ [P09, [P01_HANDLE, PRIVACY_SPIKE, P03]], // spec said P09's shape waits on P03 — make the edge real
116
+ ];
117
+ for (const [id, deps] of DEP_SETS) {
118
+ if (!id) continue;
119
+ const r = await api('PUT', `/tasks/${id}/dependencies`, { depends_on_task_ids: deps });
120
+ console.log(` task ${id} depends on [${deps.join(', ')}]: ${r.ok ? 'ok' : 'FAILED ' + r.status + ' ' + JSON.stringify(r.data).slice(0, 160)}`);
121
+ }
122
+
123
+ console.log('Setting requires_rank…');
124
+ for (const t of NEW_TASKS.filter((x) => x.rank)) {
125
+ const id = idByRef[t.ref];
126
+ if (!id) continue;
127
+ const r = await api('PATCH', `/tasks/${id}`, { requires_rank: t.rank });
128
+ console.log(` ${t.ref}: requires_rank=${t.rank} ${r.ok ? 'ok' : 'FAILED ' + JSON.stringify(r.data).slice(0, 120)}`);
129
+ }
130
+
131
+ console.log('Appending ADR 0171 amendment sections…');
132
+ const sub = (s) => s.replace('{{D01}}', idByRef.D01 || '<D01>').replace('{{D03}}', idByRef.D03 || '<D03>').replace('{{D04}}', idByRef.D04 || '<D04>');
133
+ for (const [id, text] of AMEND) {
134
+ const g = await api('GET', `/tasks/${id}`);
135
+ if (!g.ok) { console.log(` task ${id}: read FAILED ${g.status}`); continue; }
136
+ const task = g.data.task || g.data;
137
+ const body = task.description || '';
138
+ if (body.includes(MARK)) { console.log(` task ${id}: already amended, skipping`); continue; }
139
+ const next = `${body}\n\n${MARK}\n${sub(text)}`;
140
+ const r = await api('PATCH', `/tasks/${id}`, { description: next });
141
+ console.log(` task ${id}: ${r.ok ? 'amended' : 'FAILED ' + r.status + ' ' + JSON.stringify(r.data).slice(0, 160)}`);
142
+ }
143
+
144
+ console.log('Promoting zero-dep roots to ready…');
145
+ for (const t of NEW_TASKS.filter((x) => x.ready)) {
146
+ const id = idByRef[t.ref];
147
+ if (!id) continue;
148
+ const r = await api('POST', `/tasks/${id}/promote`, {});
149
+ console.log(` ${t.ref} (task ${id}): ${r.ok ? 'ready' : 'promote note ' + JSON.stringify(r.data).slice(0, 120)}`);
150
+ }
151
+
152
+ console.log(`\nDONE. created=${created} existing=${existing}.`);
153
+ console.log('ref→id:', JSON.stringify(idByRef));
154
+ })();
package/src/module-api.js CHANGED
@@ -717,8 +717,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
717
717
  // landed since 1.19.143 with no explicit bump. run 31894547582. (task 1002620)
718
718
  // 1.19.145 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
719
719
  // landed since 1.19.144 with no explicit bump. run 31895007353. (task 1002620)
720
+ // 1.19.146 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
721
+ // landed since 1.19.145 with no explicit bump. run 31904091158. (task 1002620)
720
722
  // ---------------------------------------------------------------------------
721
- const CORE_VERSION = '1.19.145'; // CI auto-patch carrier (ADR 0161); see changelog above
723
+ const CORE_VERSION = '1.19.146'; // CI auto-patch carrier (ADR 0161); see changelog above
722
724
 
723
725
  // A namespaced logger so a module's log lines are attributable + consistent.
724
726
  // Usage: const log = api.logger('dev-box'); log.info('mounted');