@bongos/core 1.19.29 → 1.19.30
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 +24 -14
- package/docs/adr/0163-legacy-display-name-resolution.md +44 -0
- package/docs/adr/README.md +2 -0
- package/migrations/core_214_display_name_source_backfill.sql +55 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/db-kernel.js +13 -0
- package/src/module-api.js +3 -1
- package/tests/display_name.mjs +76 -12
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.30",
|
|
6
|
+
"core_contract": "1.19.30",
|
|
7
|
+
"source_commit": "103b0c420b448f90a8f42f923f4e67891c45b210",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-11T14:
|
|
9
|
+
"built_at": "2026-08-11T14:41:56.292Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 286,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1206,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1513,
|
|
20
|
+
"tree_sha256": "66f1b7a904b38f7980ead45fe34e98228990e4be951b312f807251687b13d098",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1269,10 +1269,15 @@
|
|
|
1269
1269
|
"mode": "0000644",
|
|
1270
1270
|
"sha256": "f6176799c7731da3d4970b75e882e0ab88e31b1a412c87957b9a5085bfc799c7"
|
|
1271
1271
|
},
|
|
1272
|
+
{
|
|
1273
|
+
"path": "docs/adr/0163-legacy-display-name-resolution.md",
|
|
1274
|
+
"mode": "0000644",
|
|
1275
|
+
"sha256": "f1d673faaffd4d8a92709bafdc0d5c33b715131beac8f12c02dfa59880242a64"
|
|
1276
|
+
},
|
|
1272
1277
|
{
|
|
1273
1278
|
"path": "docs/adr/README.md",
|
|
1274
1279
|
"mode": "0000644",
|
|
1275
|
-
"sha256": "
|
|
1280
|
+
"sha256": "acaedbcf1451bdd23461b4d2e2e567678d1d5e4574ad615042a6c3d3db845fcc"
|
|
1276
1281
|
},
|
|
1277
1282
|
{
|
|
1278
1283
|
"path": "docs/api-reference.md",
|
|
@@ -2194,6 +2199,11 @@
|
|
|
2194
2199
|
"mode": "0000644",
|
|
2195
2200
|
"sha256": "cfc4208327550710a64e44a401f552d78ab28814f033627f5fccc174f1c15b9d"
|
|
2196
2201
|
},
|
|
2202
|
+
{
|
|
2203
|
+
"path": "migrations/core_214_display_name_source_backfill.sql",
|
|
2204
|
+
"mode": "0000644",
|
|
2205
|
+
"sha256": "5d970696a87e6f663f9fda83a268dbe99ced00ac02fb805d91114f54d4a8fece"
|
|
2206
|
+
},
|
|
2197
2207
|
{
|
|
2198
2208
|
"path": "modules/autonomy/CLAUDE.md",
|
|
2199
2209
|
"mode": "0000644",
|
|
@@ -4002,12 +4012,12 @@
|
|
|
4002
4012
|
{
|
|
4003
4013
|
"path": "package-lock.json",
|
|
4004
4014
|
"mode": "0000644",
|
|
4005
|
-
"sha256": "
|
|
4015
|
+
"sha256": "f1d885c73d1c6d6c7a1b5762e022f575045e320aa8e0fcfcaf3e9f00153fe8d5"
|
|
4006
4016
|
},
|
|
4007
4017
|
{
|
|
4008
4018
|
"path": "package.json",
|
|
4009
4019
|
"mode": "0000644",
|
|
4010
|
-
"sha256": "
|
|
4020
|
+
"sha256": "ef7a211fb70fc0316e3146e7a35a09617ea7fc2e275e8b06af61da04da94db51"
|
|
4011
4021
|
},
|
|
4012
4022
|
{
|
|
4013
4023
|
"path": "public-docs/index.html",
|
|
@@ -5402,7 +5412,7 @@
|
|
|
5402
5412
|
{
|
|
5403
5413
|
"path": "src/bongos/db-kernel.js",
|
|
5404
5414
|
"mode": "0000644",
|
|
5405
|
-
"sha256": "
|
|
5415
|
+
"sha256": "fa80ba2c0bb742b83d637280e65965f450bc0387063e65dd06f6bd9171283f23"
|
|
5406
5416
|
},
|
|
5407
5417
|
{
|
|
5408
5418
|
"path": "src/bongos/db.js",
|
|
@@ -5627,7 +5637,7 @@
|
|
|
5627
5637
|
{
|
|
5628
5638
|
"path": "src/module-api.js",
|
|
5629
5639
|
"mode": "0000644",
|
|
5630
|
-
"sha256": "
|
|
5640
|
+
"sha256": "696cdcbb043dc4b0792302807c0130d348e83d7d2e8b2fc992805f31a4a3d677"
|
|
5631
5641
|
},
|
|
5632
5642
|
{
|
|
5633
5643
|
"path": "src/module-loader/loader.js",
|
|
@@ -6287,7 +6297,7 @@
|
|
|
6287
6297
|
{
|
|
6288
6298
|
"path": "tests/display_name.mjs",
|
|
6289
6299
|
"mode": "0000644",
|
|
6290
|
-
"sha256": "
|
|
6300
|
+
"sha256": "f5074a25ef511149149a6d708561ac639eff333519f703f015b6e72dc6adb56d"
|
|
6291
6301
|
},
|
|
6292
6302
|
{
|
|
6293
6303
|
"path": "tests/dns_preflight.mjs",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# 0163 — Pre-197 display names: a transitional 'legacy' source, resolved at first sign-in
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-08-11
|
|
5
|
+
- **Deciders:** Claude (jaxri session), under the bug-wave mandate
|
|
6
|
+
|
|
7
|
+
**Track:** `internal` (GDS / builder experience). Task [1002643](https://cloudbongos.com/builders#/task/1002643), from idea 1000659. Extends [ADR 0156](<redacted>.md).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
ADR 0156 (migration `core_197`) made a builder's self-chosen display name durable by recording *who last set it* (`builders.display_name_source`: `'github' | 'user'`) and guarding the sign-in upsert. It claimed `'github'` was "the correct default for every pre-existing row: whatever is stored survived the last sign-in, so it *is* the GitHub-derived value."
|
|
14
|
+
|
|
15
|
+
That claim has a hole: a builder who renamed via `PATCH /me/display-name` **after their last sign-in but before 197 landed** still held their builder-set name in the row — 197 stamped it `'github'` anyway, so their next sign-in clobbers it. The fix shipped forward-only; the bug it fixed kept firing for exactly the people who had already hit it.
|
|
16
|
+
|
|
17
|
+
A precise backfill is impossible: the rename route writes no `audit_log` entry, and nothing stores what GitHub reported per row. "Was this stored name builder-set?" has no answer in the data.
|
|
18
|
+
|
|
19
|
+
## Decision
|
|
20
|
+
|
|
21
|
+
**A third, transitional `display_name_source` value — `'legacy'` — for every row that predates 197 on the instance, resolved definitively by the upsert at each row's first subsequent sign-in.**
|
|
22
|
+
|
|
23
|
+
- Migration `core_214` extends the CHECK constraint and marks `'legacy'` every row with `display_name_source = 'github'` created before `core_197`'s `applied_at` **on that instance** (read from `schema_migrations`, so the scoping is portable to self-hosted instances regardless of when they upgraded). A replay guard ensures resolved rows are never re-marked.
|
|
24
|
+
- `upsertBuilder`'s conflict branch resolves each `'legacy'` row exactly once, using the one comparison that is only available at sign-in time — the incoming GitHub-derived name:
|
|
25
|
+
- stored name **equals** incoming → provably GitHub-derived → `'github'`; re-sync resumes as if 197's default had been right.
|
|
26
|
+
- stored name **differs** → treated as builder-set → kept, stamped `'user'`; durable from then on.
|
|
27
|
+
- `'user'` and `'github'` remain terminal; post-197 rows are untouched.
|
|
28
|
+
|
|
29
|
+
### The accepted misclassification
|
|
30
|
+
|
|
31
|
+
A `'legacy'` row whose *GitHub-side* name changed between its last pre-214 sync and its first post-214 sign-in also "differs" — the stale GitHub name gets frozen as `'user'`. That trade is deliberate: the wrong outcome there is a stale *label*, visible and self-fixable in Settings; the wrong outcome under the status quo is the silent destruction of a deliberate rename — the reported bug. Preserve wins over sync when the evidence is ambiguous.
|
|
32
|
+
|
|
33
|
+
### Rejected alternatives
|
|
34
|
+
|
|
35
|
+
- **Backfill `'user'` where `display_name <> github_login`** — overwhelmingly wrong: most GitHub accounts have a profile name that differs from the login, so this would freeze GitHub-derived names for nearly everyone, silently reintroducing the frozen-name bug ADR 0156 rejected.
|
|
36
|
+
- **Backfill nothing, compare names for ALL `'github'` rows forever** — turns every future GitHub rename into a frozen name; `'legacy'` scopes the ambiguity to the rows that actually carry it and expires it one sign-in later.
|
|
37
|
+
- **Restore lost names from an audit trail** — there is no audit trail; rows already clobbered between 197 and 214 are unrecoverable, and this ADR does not pretend otherwise.
|
|
38
|
+
|
|
39
|
+
## Consequences
|
|
40
|
+
|
|
41
|
+
- `builders_display_name_source_check` now allows `'legacy'`; the value disappears from a row at its first post-214 sign-in and is never written by any other path.
|
|
42
|
+
- `tests/display_name.mjs` evaluates the real conflict-branch SQL for all three source states and pins the migration's contract (only-`'github'` rows marked, `applied_at` scoping, replay guard).
|
|
43
|
+
- Not surfaced in the API — same posture as ADR 0156; no UI copy branches on it.
|
|
44
|
+
- Builders whose names were already re-clobbered between 197 and 214 must re-save once in Settings — with 214 in place, it now sticks.
|
package/docs/adr/README.md
CHANGED
|
@@ -235,4 +235,6 @@ This keeps the decision history honest and traceable.
|
|
|
235
235
|
| 0161 | [**Publish on merge: every merge to core `main` auto-publishes `@bongos/core`** (task 1002620). Supersedes ADR 0137's manual owner-gated publish after its own "when to revisit" clause fired twice in three weeks: 171 commits/10 days, then 104 commits/5 days, sat on `main` reachable by no pin while every layer reported success. A `publish` workflow rides behind the required `unit` check (`workflow_run`, success-gated): when HEAD's `CORE_VERSION` is already on npm, CI bumps a PATCH via the shape-preserving `bump-version.js` (+ changelog line; pinned by `tests/bump_version.mjs`) and pushes the carrier commit; `package-core --self-verify --publish --live` builds through the fail-closed no-leak gate; the release is tagged `core-v<v>`. A MINOR/MAJOR stays a human in-task edit — it decides what patch-channel subscribers receive unattended (ADR 0136), a call CI must not make. Token custody knowingly reverses (owner-accepted): `NPM_TOKEN` write + `RELEASE_PUSH_TOKEN` PAT as Actions secrets, mitigated by the lane shipping INERT behind the `PUBLISH_ON_MERGE=1` repo variable — an owner kill-switch symmetrical to `gating.js`. Idempotent by version-compare; npm duplicate rejection is the backstop; queued no-cancel concurrency collapses merge races.](<redacted>.md) | build pipeline / distribution |
|
|
236
236
|
| 0162 | [**Review gates retired: rank-consistent CI checks only on `main`** ([task 1002635](https://cloudbongos.com/builders#/task/1002635); owner directive, idea 1000657). Amends ADR 0159. Within a day of task 1002619 arming the merge gates, the code-owner review requirement had stranded two graded-PASS wave-4 ships (PR 28 sat BLOCKED behind a review only the repo-owner account could give) and then blocked the fixes for the strands it caused — structurally, not incidentally: GitHub can only express the gate as *accounts* (no rank concept, no self-approval), so on a solo-archon instance it degenerates into "one named login must click every gate-surface PR" — identity-pinned authority, against ADR 0016's rank principle and the remediation's own goal. Decision: branch protection carries required status checks ONLY; `gating.js` `DEFAULT_PROTECTION` sets `required_pull_request_reviews: null` and `cmdOn` normalizes BOTH restore paths so a pre-retirement snapshot cannot resurrect the gate (`tests/gating_review_retired.mjs` pins it with a counter-example); the CODEOWNERS *file* is deleted while its *glob* stays in the hard-floor lists so reintroducing it is itself gate-flagged. Accepted trade, chosen explicitly by the owner: no pre-merge human approval on safety-machinery changes — the unit check, the fail-closed gate-review sandbox, the grader panel, and the every-push main-audit are the net, catching a compromised session's merge after the fact rather than before.](<redacted>.md) | governance / build pipeline |
|
|
237
237
|
|
|
238
|
+
| 0163 | [**Pre-197 display names: a transitional 'legacy' source, resolved at first sign-in** ([task 1002643](https://cloudbongos.com/builders#/task/1002643); idea 1000659). Amends [ADR 0156](<redacted>.md), whose migration defaulted every pre-existing row to `'github'` — false for builders who renamed after their last sign-in but before 197 landed, so their names kept reverting. No audit trail exists to backfill precisely, so `core_214` marks every pre-197 `'github'` row `'legacy'` (scoped by 197's `applied_at` on the instance — portable) and the upsert resolves each at its first sign-in: stored = incoming GitHub name → provably GitHub-derived → `'github'`; differs → treated as builder-set → kept, stamped `'user'`. Accepted misclassification: a GitHub-side rename in the window freezes a stale label (visible, self-fixable in Settings) — preferred over silently destroying deliberate renames. Rejected: `display_name <> github_login` heuristic (freezes nearly everyone), comparing forever on all `'github'` rows (turns every future GitHub rename into a frozen name).](<redacted>.md) | GDS / builder experience |
|
|
239
|
+
|
|
238
240
|
> ⚠️ **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`.)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
-- core_214_display_name_source_backfill.sql — heal pre-197 user-set names (task 1002643).
|
|
2
|
+
--
|
|
3
|
+
-- THE DEFECT. core_197 added display_name_source with DEFAULT 'github' for every
|
|
4
|
+
-- existing row, reasoning that "whatever is stored survived the last sign-in, so
|
|
5
|
+
-- it IS the GitHub-derived value" (ADR 0156). That is false for a builder who
|
|
6
|
+
-- renamed via PATCH /me/display-name AFTER their last sign-in but BEFORE 197
|
|
7
|
+
-- landed: their stored name is builder-set, the row says 'github', and their
|
|
8
|
+
-- next sign-in still clobbers it — the exact bug 197 shipped to fix. The fix was
|
|
9
|
+
-- forward-only; this migration is the missing backfill.
|
|
10
|
+
--
|
|
11
|
+
-- WHY NOT A PRECISE BACKFILL. The rename route writes no audit_log entry and
|
|
12
|
+
-- nothing records what GitHub reported per row, so "was this name builder-set?"
|
|
13
|
+
-- is unanswerable from stored data. Instead: a transitional third source value,
|
|
14
|
+
-- 'legacy', for every row that predates 197 on THIS instance. The upsert guard
|
|
15
|
+
-- (src/bongos/db-kernel.js) resolves each 'legacy' row definitively at its next
|
|
16
|
+
-- sign-in: stored name equals what GitHub sends, so it is provably GitHub-derived,
|
|
17
|
+
-- back to 'github' (re-sync resumes); differs, so treated as builder-set: kept, 'user'.
|
|
18
|
+
--
|
|
19
|
+
-- ACCEPTED TRADE (ADR 0163): a legacy row whose GitHub-side name changed before
|
|
20
|
+
-- its first post-214 sign-in freezes the stale GitHub name as 'user' — a
|
|
21
|
+
-- visible label, self-fixable in Settings. The status quo instead silently
|
|
22
|
+
-- destroys deliberate renames. Preserve wins.
|
|
23
|
+
--
|
|
24
|
+
-- Scoped by created_at against 197's applied_at ON THIS INSTANCE — portable:
|
|
25
|
+
-- every instance applied 197 at its own time. When 197 and 214 apply in the
|
|
26
|
+
-- same run (fresh install / big upgrade / restore-replay), applied_at is ~now()
|
|
27
|
+
-- so every pre-existing row is covered, which is then also correct: none of
|
|
28
|
+
-- those rows ever had the guard protecting them.
|
|
29
|
+
--
|
|
30
|
+
-- Idempotent: constraint statements re-run cleanly, and the NOT EXISTS guard
|
|
31
|
+
-- makes the UPDATE a no-op on replay against an up-to-date DB — rows resolved
|
|
32
|
+
-- after the first run are never re-marked 'legacy'.
|
|
33
|
+
|
|
34
|
+
BEGIN;
|
|
35
|
+
|
|
36
|
+
ALTER TABLE builders DROP CONSTRAINT IF EXISTS builders_display_name_source_check;
|
|
37
|
+
ALTER TABLE builders
|
|
38
|
+
ADD CONSTRAINT builders_display_name_source_check
|
|
39
|
+
CHECK (display_name_source IN ('github', 'user', 'legacy'));
|
|
40
|
+
|
|
41
|
+
UPDATE builders
|
|
42
|
+
SET display_name_source = 'legacy'
|
|
43
|
+
WHERE display_name_source = 'github'
|
|
44
|
+
AND created_at < COALESCE(
|
|
45
|
+
(SELECT applied_at FROM schema_migrations
|
|
46
|
+
WHERE version = 'core_197_display_name_source'),
|
|
47
|
+
now())
|
|
48
|
+
AND NOT EXISTS (
|
|
49
|
+
SELECT 1 FROM schema_migrations
|
|
50
|
+
WHERE version = 'core_214_display_name_source_backfill');
|
|
51
|
+
|
|
52
|
+
INSERT INTO schema_migrations (version) VALUES ('core_214_display_name_source_backfill')
|
|
53
|
+
ON CONFLICT DO NOTHING;
|
|
54
|
+
|
|
55
|
+
COMMIT;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.30",
|
|
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.30",
|
|
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.30",
|
|
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/bongos/db-kernel.js
CHANGED
|
@@ -154,6 +154,10 @@ const RANKS = [
|
|
|
154
154
|
// seeding and re-syncing the name for anyone who never renamed
|
|
155
155
|
// (display_name_source='github'), and never overwrites one the builder set
|
|
156
156
|
// themselves ('user', stamped by setBuilderDisplayName). See ADR 0156.
|
|
157
|
+
// 'legacy' rows (pre-197 accounts, backfilled by core_214) are ambiguous — no
|
|
158
|
+
// record says whether their stored name was builder-set. This sign-in resolves
|
|
159
|
+
// each one: stored = incoming proves GitHub-derived → 'github'; differing is
|
|
160
|
+
// treated as builder-set → kept and stamped 'user' (ADR 0163).
|
|
157
161
|
async function upsertBuilder({ githubId, githubLogin, displayName, avatarUrl }) {
|
|
158
162
|
const { rows } = await pool.query(
|
|
159
163
|
`INSERT INTO builders (github_id, github_login, display_name, avatar_url)
|
|
@@ -162,7 +166,16 @@ async function upsertBuilder({ githubId, githubLogin, displayName, avatarUrl })
|
|
|
162
166
|
SET github_login = EXCLUDED.github_login,
|
|
163
167
|
display_name = CASE WHEN builders.display_name_source = 'user'
|
|
164
168
|
THEN builders.display_name
|
|
169
|
+
WHEN builders.display_name_source = 'legacy'
|
|
170
|
+
AND builders.display_name IS DISTINCT FROM EXCLUDED.display_name
|
|
171
|
+
THEN builders.display_name
|
|
165
172
|
ELSE EXCLUDED.display_name END,
|
|
173
|
+
display_name_source = CASE WHEN builders.display_name_source = 'user'
|
|
174
|
+
THEN 'user'
|
|
175
|
+
WHEN builders.display_name_source = 'legacy'
|
|
176
|
+
AND builders.display_name IS DISTINCT FROM EXCLUDED.display_name
|
|
177
|
+
THEN 'user'
|
|
178
|
+
ELSE 'github' END,
|
|
166
179
|
avatar_url = EXCLUDED.avatar_url
|
|
167
180
|
RETURNING id, github_id, github_login, display_name, avatar_url, total_credits, created_at, rank, preferred_disciplines`,
|
|
168
181
|
[String(githubId), githubLogin, displayName, avatarUrl ?? null]
|
package/src/module-api.js
CHANGED
|
@@ -485,8 +485,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
485
485
|
// landed since 1.19.27 with no explicit bump. run 31499228797. (task 1002620)
|
|
486
486
|
// 1.19.29 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
487
487
|
// landed since 1.19.28 with no explicit bump. run 31500810303. (task 1002620)
|
|
488
|
+
// 1.19.30 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
489
|
+
// landed since 1.19.29 with no explicit bump. run 31502880105. (task 1002620)
|
|
488
490
|
// ---------------------------------------------------------------------------
|
|
489
|
-
const CORE_VERSION = '1.19.
|
|
491
|
+
const CORE_VERSION = '1.19.30'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
490
492
|
|
|
491
493
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
492
494
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/display_name.mjs
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { strict as assert } from 'node:assert';
|
|
21
21
|
import { createRequire } from 'node:module';
|
|
22
|
+
import { readFileSync } from 'node:fs';
|
|
22
23
|
|
|
23
24
|
const require = createRequire(import.meta.url);
|
|
24
25
|
const d = require('../modules/builder-settings/display-name.js');
|
|
@@ -151,20 +152,38 @@ function conflictAssignment(sql, column) {
|
|
|
151
152
|
throw new Error(`no "${column} = …" assignment in the conflict branch`);
|
|
152
153
|
}
|
|
153
154
|
|
|
154
|
-
// Evaluate
|
|
155
|
-
// behaviour ("whose name ends up stored?") rather than string matching.
|
|
155
|
+
// Evaluate a conflict-branch assignment for one row, so the tests below read as
|
|
156
|
+
// behaviour ("whose name ends up stored?") rather than string matching. Handles
|
|
157
|
+
// the guard's exact grammar: multi-arm CASE, AND-joined conditions on
|
|
158
|
+
// display_name_source and IS DISTINCT FROM, string-literal results. An
|
|
156
159
|
// unguarded assignment — the shape that shipped the bug — is a bare column
|
|
157
160
|
// reference and evaluates to whatever GitHub just sent.
|
|
158
|
-
function
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
if (/^
|
|
162
|
-
|
|
161
|
+
function resolvedAssignment(expr, { source, stored, incoming }) {
|
|
162
|
+
const val = (token) => {
|
|
163
|
+
const t = token.trim();
|
|
164
|
+
if (/^EXCLUDED\.display_name$/i.test(t)) return incoming;
|
|
165
|
+
if (/^builders\.display_name$/i.test(t)) return stored;
|
|
166
|
+
const lit = /^'(\w+)'$/.exec(t);
|
|
167
|
+
if (lit) return lit[1];
|
|
168
|
+
throw new Error(`unrecognised token: ${t}`);
|
|
163
169
|
};
|
|
170
|
+
const cond = (c) => c.split(/\s+AND\s+/i).every((one) => {
|
|
171
|
+
const t = one.trim();
|
|
172
|
+
let m = /^builders\.display_name_source = '(\w+)'$/i.exec(t);
|
|
173
|
+
if (m) return source === m[1];
|
|
174
|
+
m = /^builders\.display_name IS DISTINCT FROM EXCLUDED\.display_name$/i.exec(t);
|
|
175
|
+
if (m) return stored !== incoming;
|
|
176
|
+
throw new Error(`unrecognised condition: ${t}`);
|
|
177
|
+
});
|
|
164
178
|
const norm = expr.replace(/\s+/g, ' ').trim();
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
179
|
+
if (!/^CASE WHEN /i.test(norm)) return val(norm);
|
|
180
|
+
const inner = norm.replace(/^CASE\s+/i, '').replace(/\s+END$/i, '');
|
|
181
|
+
const elseM = /\sELSE\s+(.+)$/i.exec(inner);
|
|
182
|
+
if (!elseM) throw new Error(`CASE without ELSE: ${norm}`);
|
|
183
|
+
for (const [, c, result] of inner.slice(0, elseM.index).matchAll(/WHEN\s+(.+?)\s+THEN\s+(\S+)/gi)) {
|
|
184
|
+
if (cond(c)) return val(result);
|
|
185
|
+
}
|
|
186
|
+
return val(elseM[1]);
|
|
168
187
|
}
|
|
169
188
|
|
|
170
189
|
// Drive both halves once (they're async), then assert on the captured SQL with
|
|
@@ -173,6 +192,7 @@ await kernel.upsertBuilder({
|
|
|
173
192
|
githubId: 64553469, githubLogin: 'jaxri', displayName: 'GitHub Name', avatarUrl: null,
|
|
174
193
|
});
|
|
175
194
|
const upsertExpr = conflictAssignment(lastSql, 'display_name');
|
|
195
|
+
const sourceExpr = conflictAssignment(lastSql, 'display_name_source');
|
|
176
196
|
|
|
177
197
|
await settingsDb.setBuilderDisplayName(1000095, 'Renée the Builder');
|
|
178
198
|
const setNameSql = lastSql.replace(/\s+/g, ' ');
|
|
@@ -187,14 +207,14 @@ t('the conflict branch keys display_name off display_name_source', () => {
|
|
|
187
207
|
});
|
|
188
208
|
|
|
189
209
|
t('a builder who set their own name keeps it when GitHub sends a different one', () => {
|
|
190
|
-
const kept =
|
|
210
|
+
const kept = resolvedAssignment(upsertExpr, {
|
|
191
211
|
source: 'user', stored: 'Renée the Builder', incoming: 'renee-gh',
|
|
192
212
|
});
|
|
193
213
|
assert.equal(kept, 'Renée the Builder');
|
|
194
214
|
});
|
|
195
215
|
|
|
196
216
|
t('a builder who never renamed still picks up a GitHub rename', () => {
|
|
197
|
-
const synced =
|
|
217
|
+
const synced = resolvedAssignment(upsertExpr, {
|
|
198
218
|
source: 'github', stored: 'Old GitHub Name', incoming: 'New GitHub Name',
|
|
199
219
|
});
|
|
200
220
|
assert.equal(synced, 'New GitHub Name');
|
|
@@ -208,5 +228,49 @@ t('setBuilderDisplayName stamps the name as user-set', () => {
|
|
|
208
228
|
);
|
|
209
229
|
});
|
|
210
230
|
|
|
231
|
+
// --- pre-197 rows: 'legacy' resolves at first sign-in (task 1002643, ADR 0163)
|
|
232
|
+
|
|
233
|
+
console.log('\nlegacy resolution (pre-197 rows backfilled by core_214):');
|
|
234
|
+
|
|
235
|
+
t('a legacy row whose name differs from GitHub keeps it — and becomes user-set', () => {
|
|
236
|
+
const row = { source: 'legacy', stored: 'Percy Jackson', incoming: 'GitHub Name' };
|
|
237
|
+
assert.equal(resolvedAssignment(upsertExpr, row), 'Percy Jackson');
|
|
238
|
+
assert.equal(resolvedAssignment(sourceExpr, row), 'user');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
t('a legacy row whose name matches GitHub resolves to github and resumes syncing', () => {
|
|
242
|
+
const row = { source: 'legacy', stored: 'Same Name', incoming: 'Same Name' };
|
|
243
|
+
assert.equal(resolvedAssignment(upsertExpr, row), 'Same Name');
|
|
244
|
+
assert.equal(resolvedAssignment(sourceExpr, row), 'github');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
t('resolved sources are terminal: user stays user, github stays github', () => {
|
|
248
|
+
assert.equal(resolvedAssignment(sourceExpr, { source: 'user', stored: 'A', incoming: 'B' }), 'user');
|
|
249
|
+
assert.equal(resolvedAssignment(sourceExpr, { source: 'github', stored: 'A', incoming: 'B' }), 'github');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// The backfill migration is load-bearing for the guard above: without it no row
|
|
253
|
+
// is ever 'legacy'. Pin its contract so a hand-edit can't silently unwind it.
|
|
254
|
+
const migration = readFileSync(
|
|
255
|
+
new URL('../migrations/core_214_display_name_source_backfill.sql', import.meta.url),
|
|
256
|
+
'utf8'
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
t("core_214 extends the CHECK constraint to allow 'legacy'", () => {
|
|
260
|
+
assert.match(migration, /CHECK \(display_name_source IN \('github', 'user', 'legacy'\)\)/);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
t("core_214 marks only 'github' rows — a user-set stamp is never touched", () => {
|
|
264
|
+
assert.match(migration, /SET display_name_source = 'legacy'\s+WHERE display_name_source = 'github'/);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
t('core_214 scopes the backfill to rows created before 197 applied on this instance', () => {
|
|
268
|
+
assert.match(migration, /created_at < COALESCE\(\s*\(SELECT applied_at FROM schema_migrations\s+WHERE version = 'core_197_display_name_source'\)/);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
t('core_214 is replay-safe: resolved rows are never re-marked legacy', () => {
|
|
272
|
+
assert.match(migration, /NOT EXISTS \(\s*SELECT 1 FROM schema_migrations\s+WHERE version = 'core_214_display_name_source_backfill'\)/);
|
|
273
|
+
});
|
|
274
|
+
|
|
211
275
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
212
276
|
process.exit(failed === 0 ? 0 : 1);
|