@bongos/core 1.19.620 → 1.19.622

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.620",
6
- "core_contract": "1.19.620",
7
- "source_commit": "7e8c8d38a15a24575258eda58829e9120d9ed55f",
5
+ "core_version": "1.19.622",
6
+ "core_contract": "1.19.622",
7
+ "source_commit": "d7fca718550089ea07dc0180b88bcf356c86e06f",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-09T06:53:24.739Z",
9
+ "built_at": "2026-09-09T07:05:49.899Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 464,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2086,
14
+ "functional_verbatim": 2087,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2574,
20
- "tree_sha256": "8c5d0cef512909da1de5dd90510ca13a74737c8d3b80ec2d8e0a7fc5e1d9451d",
19
+ "file_count": 2575,
20
+ "tree_sha256": "b42f3efc74c1736882f7545a20d0ab217990c61535f00a1c38b916f8ee92f029",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/backlog-review/SKILL.md",
@@ -377,7 +377,7 @@
377
377
  {
378
378
  "path": "clients/bongos-client/README.md",
379
379
  "mode": "0000644",
380
- "sha256": "7486f9978e473b0adf09f8bfcbe2a029eb2833636ae3bd55fc307ccb3e773963"
380
+ "sha256": "65fb7649709e966a90f431d57b8855bd39951684a754320c4a797da9e3ccac13"
381
381
  },
382
382
  {
383
383
  "path": "clients/bongos-client/bongos-client.global.js",
@@ -407,7 +407,7 @@
407
407
  {
408
408
  "path": "clients/bongos-client/package.json",
409
409
  "mode": "0000644",
410
- "sha256": "ca32245dc6322f0399058b91f07fe12709fe7b97485fd5d8f5f37cb42f730aae"
410
+ "sha256": "d299252221daca415cf982b61fe5e3d9c3707c672a7d47adc052a9ee8166223f"
411
411
  },
412
412
  {
413
413
  "path": "config/branding.neutral.json",
@@ -1172,7 +1172,7 @@
1172
1172
  {
1173
1173
  "path": "docs/adr/0134-private-first-npm-distribution.md",
1174
1174
  "mode": "0000644",
1175
- "sha256": "ce1345a7de47416745964de31ec0d72cdf37f29fc1d41337b24d0e705710b0d7"
1175
+ "sha256": "87b5f5038221e8244c3f98e6f9bc6bf64b23cf8cc758327dd5a58c9b31196fb2"
1176
1176
  },
1177
1177
  {
1178
1178
  "path": "docs/adr/0135-module-upstream-submission-interim-queue.md",
@@ -1812,7 +1812,7 @@
1812
1812
  {
1813
1813
  "path": "docs/adr/0258-the-public-cli-is-a-generated-client-package-not-the-published-core.md",
1814
1814
  "mode": "0000644",
1815
- "sha256": "43bf2232da3353ab49bfd956a73d09c5045e5fa155e0004d0855a50587c299cd"
1815
+ "sha256": "e701aa98eddf2a0bde803fecfaf681502db33e67765f6a355e883cd3c714cd08"
1816
1816
  },
1817
1817
  {
1818
1818
  "path": "docs/adr/0259-a-projects-departure-from-the-public-list-is-public.md",
@@ -1872,7 +1872,7 @@
1872
1872
  {
1873
1873
  "path": "docs/adr/README.md",
1874
1874
  "mode": "0000644",
1875
- "sha256": "d660b4bc0bc3306f5fcf583118ca9149ba3deafe82dfe3894fe0b1c11b60cc2f"
1875
+ "sha256": "6b90114abfa5e891a8f2ea16c547c960c982030d6228d43c2a833840123a3f69"
1876
1876
  },
1877
1877
  {
1878
1878
  "path": "docs/api-reference.md",
@@ -2757,7 +2757,7 @@
2757
2757
  {
2758
2758
  "path": "docs/module-api-changelog.md",
2759
2759
  "mode": "0000644",
2760
- "sha256": "6b3c53cfb31b4bf6963f740bb386c24059ef370af8927b0ececde09e062fff33"
2760
+ "sha256": "502e05437441903b3c3a5388f43bfc60e1c910005a6ac0ff7ff07e5d2199a3e1"
2761
2761
  },
2762
2762
  {
2763
2763
  "path": "docs/modules-contract.md",
@@ -2947,7 +2947,7 @@
2947
2947
  {
2948
2948
  "path": "docs/recipes/private-npm-distribution.md",
2949
2949
  "mode": "0000644",
2950
- "sha256": "3c6613920881220ec4a076b4608ed57557d645c43dee3fbb4086ad4adb81f593"
2950
+ "sha256": "87851cddc742fb4c62de1cde8b0443a6b6fd800e753e715ed8d2ab838557d4b8"
2951
2951
  },
2952
2952
  {
2953
2953
  "path": "docs/recipes/redteam-patrol.md",
@@ -5597,7 +5597,7 @@
5597
5597
  {
5598
5598
  "path": "modules/lifecycle/db-tasks.js",
5599
5599
  "mode": "0000644",
5600
- "sha256": "15c9efea1fb5a8187fe5bae756777ebeb1a214e47e0434d7a8adeb156bd1e093"
5600
+ "sha256": "65558689ccfcea8f71b98cd51eaf1fb45d39267f7e6f6cf8fc35814da2752a34"
5601
5601
  },
5602
5602
  {
5603
5603
  "path": "modules/lifecycle/db-versions.js",
@@ -7662,12 +7662,12 @@
7662
7662
  {
7663
7663
  "path": "package-lock.json",
7664
7664
  "mode": "0000644",
7665
- "sha256": "401ae455c8e67d7bf4d622ef7e93fce203948868ae3b07de9770e6d90607249f"
7665
+ "sha256": "f56943090b50301c55cc7bb459c1d2b805aaaee4dc57953071c80268ea3bcac0"
7666
7666
  },
7667
7667
  {
7668
7668
  "path": "package.json",
7669
7669
  "mode": "0000644",
7670
- "sha256": "f6c0e1047dcef4fe6e429e149606d521e0d196ff748bcdeb6de6e1c63d2a60ab"
7670
+ "sha256": "0bf4bc86183612ce855831f54233cec5ff5f2f9cbf56925dcc63d28a50338636"
7671
7671
  },
7672
7672
  {
7673
7673
  "path": "public-docs/index.html",
@@ -7872,7 +7872,7 @@
7872
7872
  {
7873
7873
  "path": "scripts/gds/build-cli-package.js",
7874
7874
  "mode": "0000644",
7875
- "sha256": "de1cdede17761775722509b1e1edb4bb43fbbf336450556f22119b18b93e43d3"
7875
+ "sha256": "e24710d5546428beba9400831d69eb3101e76bfdb1083916bf12411839e16735"
7876
7876
  },
7877
7877
  {
7878
7878
  "path": "scripts/gds/bump-version.js",
@@ -8157,7 +8157,7 @@
8157
8157
  {
8158
8158
  "path": "scripts/gds/gen-api-client.js",
8159
8159
  "mode": "0000644",
8160
- "sha256": "e9a6c6498f41b51dbcc17a3538d997b220f671beb433ae65ebc5365727a444ec"
8160
+ "sha256": "d363c76caf379a051faea40a2497db8bbba18effd68e9cd9922e9445638c57c5"
8161
8161
  },
8162
8162
  {
8163
8163
  "path": "scripts/gds/gen-api-docs.js",
@@ -8922,7 +8922,7 @@
8922
8922
  {
8923
8923
  "path": "scripts/gds/start.js",
8924
8924
  "mode": "0000644",
8925
- "sha256": "061a477c618ca76aba4f3abfb2d22ad89ceb0bee46bac65b139ad8ee2f30413a"
8925
+ "sha256": "d0adb1267fc2d75482a37ba252ebffce15cdd32ef2af178579668776a8dfa3a4"
8926
8926
  },
8927
8927
  {
8928
8928
  "path": "scripts/gds/status.js",
@@ -9397,7 +9397,7 @@
9397
9397
  {
9398
9398
  "path": "src/module-api.js",
9399
9399
  "mode": "0000644",
9400
- "sha256": "d2b45162be47422810ab7a05c42c9926520df6c1c5ab7e7274386c5a25eb07a3"
9400
+ "sha256": "aa73b7f2356cde88eb58d423517adc364cf6fb7e8c9fd5dc177ae4d73b83e3e2"
9401
9401
  },
9402
9402
  {
9403
9403
  "path": "src/module-loader/catalog.js",
@@ -10159,6 +10159,11 @@
10159
10159
  "mode": "0000644",
10160
10160
  "sha256": "1ab482fea7d56cccd811c760e21bf564c7952cd5c11d5831faa500b6c2eaa331"
10161
10161
  },
10162
+ {
10163
+ "path": "tests/cross_discipline_priority_direction.mjs",
10164
+ "mode": "0000644",
10165
+ "sha256": "d7e67ada5978b56e271439233c4c8cd7bcb2abfada593c4591dee4904a8a6233"
10166
+ },
10162
10167
  {
10163
10168
  "path": "tests/currency_label.mjs",
10164
10169
  "mode": "0000644",
@@ -7,14 +7,26 @@ by hand; it regenerates when the spec changes, so it can never drift from the ro
7
7
  - API version: **v1** (served at `/api/bongos/v1`)
8
8
  - 360 operations across 55 resource groups
9
9
 
10
- ## Install
10
+ ## Use it from your project
11
+
12
+ **This is not an npm package** — it is not published, and nothing depends on it as one
13
+ (task 1003742). Consume it by VENDORING the build you need, which is what every consumer
14
+ in this repo already does:
11
15
 
12
16
  ```bash
13
- npm install @bongos/client # internal registry / workspace path
17
+ # ESM / bundler: cp clients/bongos-client/index.mjs <your project>/vendor/
18
+ # CommonJS: cp clients/bongos-client/index.cjs <your project>/vendor/
19
+ # Browser global: cp clients/bongos-client/bongos-client.global.js <your project>/public/
14
20
  ```
15
21
 
22
+ Copy `index.d.ts` alongside it for types. Re-copy after `gen-api-client.js` runs, so the
23
+ client cannot drift from the routes.
24
+
16
25
  ## Use
17
26
 
27
+ The examples import by package name, which resolves in a workspace. A VENDORED copy is
28
+ imported by its path instead — `from './vendor/index.mjs'` — everything below is identical.
29
+
18
30
  ```js
19
31
  import { createClient, ApiError } from '@bongos/client';
20
32
 
@@ -25,8 +25,5 @@
25
25
  "README.md"
26
26
  ],
27
27
  "license": "AGPL-3.0-or-later",
28
- "publishConfig": {
29
- "access": "restricted"
30
- },
31
28
  "sideEffects": false
32
29
  }
@@ -1,6 +1,6 @@
1
1
  # 0134 — Private-first npm distribution for the core + client (paid scoped, then public at launch)
2
2
 
3
- - **Status:** Accepted — publish path + private-by-default config built in task 2089; the real publish is a gated owner step.
3
+ - **Status:** Accepted for the CORE; the **client half is superseded** by [task 1003742](https://cloudbongos.com/builders#/task/1003742) (2026-09-09) see the note under Decision. Publish path + private-by-default config built in task 2089; the real publish is a gated owner step.
4
4
  - **Date:** 2026-07-07
5
5
  - **Deciders:** example-owner (owner/Archon), Claude. Owner asked to have the npm/CLI onboarding commands ready but kept private pre-launch, choosing paid private npm over the free GitHub Packages option.
6
6
  - **Task:** [#2089](https://example.com/builders#/task/2089) (publish path + private config) · [#2092](https://example.com/builders#/task/2092) (the `@cloudbongos` → `@bongos` scope rename + first real publish) · **Goal:** 35 (project-creation flow), criterion 165.
@@ -18,6 +18,25 @@ One npm rule forces the shape: **unscoped packages are always public — only *s
18
18
 
19
19
  Publish **`@bongos/core`** and **`@bongos/client`** as **scoped, private** npm packages during pre-launch, on the owner's **paid** `bongos` npm org — the **canonical** brand org.
20
20
 
21
+ > **SUPERSEDED for the client (2026-09-09, [task 1003742](https://cloudbongos.com/builders#/task/1003742)).** `@bongos/client`
22
+ > is no longer an npm package at all, and the reason is worth keeping so it does not come back.
23
+ >
24
+ > It was published **once** — 2026-07-07, version 0.0.2 — and never updated, while the API it is
25
+ > *generated from* moved 500+ core versions. In that time it had **zero consumers**: a sweep of every
26
+ > `package.json` across the core and every instance repo (hermeslines-marketing, cloudbongos-instance,
27
+ > mercury, charter, demo) found no dependency on it; the only matches were its own manifest. Everyone
28
+ > who needs the generated client **vendors a copy** — `modules/dev-box/app/src/vendor/bongos-client.cjs`,
29
+ > and `build-cli-package.js` into `@cloudbongos/cli`, which as a *public* package could not have
30
+ > depended on a private one anyway ([ADR 0258](<redacted>.md)).
31
+ > A stale generated client that nobody installs is worse than none: the only thing it could do is
32
+ > mislead someone into pinning a client 500 versions behind the routes.
33
+ >
34
+ > The owner removed it from the registry. The **generated artifact stays** and still regenerates
35
+ > ([ADR 0118](<redacted>.md)) — `clients/bongos-client/` and
36
+ > `gen-api-client.js` earn their place. What is retired is only the claim that it is a package: the
37
+ > generator no longer emits a `publishConfig`, and its README now documents vendoring instead of
38
+ > `npm install`. **This ADR's core half is unaffected.**
39
+
21
40
  > **Scope correction (task 2092).** The publish path first shipped (task 2089) under the `@cloudbongos` scope, matching the name the repo already used. But when publishing, the paid org turned out to be **`bongos`** (the canonical brand the owner put on Teams), while `cloudbongos` — one of the ~10 defensive orgs — was left on the free tier. Since a private scoped publish requires the scope to match a *paid* org, and `@bongos` is the intended canonical brand anyway, we **renamed the core + client package identity `@cloudbongos/*` → `@bongos/*`** (a consistent rename across the build + vendored-install flow, verified by the test suite) rather than pay for a second org. Existing vendored consumers keep their pinned `@cloudbongos/core` tarball; only new scaffolds use `@bongos`.
22
41
 
23
42
  - **Private by default in code, not just by flag.** The synthesized core `package.json` (`buildPackageJson`) and the generated client `package.json` (`gen-api-client.js`) both carry `publishConfig: { access: "restricted" }`. `package-core.js --publish` additionally passes `--access restricted` on the CLI — belt-and-suspenders so a stray config edit can't silently make the pre-launch artifact public.
@@ -120,9 +120,11 @@ benefit. It gains one when ADR 0099's mirror lands.
120
120
  - **Publishing is the owner's action, not a builder's.** It is the owner's npm account, and a
121
121
  public package name cannot be un-taken. The build stops at a packed tarball and hands over one
122
122
  `npm publish --access public`.
123
- - `@bongos/client` stays private and is **vendored** as a single file rather than declared as a
124
- dependency — a public package that depended on a private one would be uninstallable for
125
- everyone.
123
+ - `@bongos/client` is **vendored** as a single file rather than declared as a dependency — a public
124
+ package that depended on a private one would be uninstallable for everyone. (It was private then;
125
+ since [task 1003742](https://cloudbongos.com/builders#/task/1003742) it is not published at all,
126
+ which makes the vendoring not merely preferable but the only option. Vendoring was the right call
127
+ either way, and this package is one of the two reasons the client had no consumers to lose.)
126
128
  - Task 1002025 is superseded and should be closed against this ADR rather than done.
127
129
  - **The journey above is instance-dependent, and 0.1.1 shipped assuming it was not**
128
130
  ([task 1003730](https://cloudbongos.com/builders#/task/1003730)). `bongos shell` is the answer
@@ -208,7 +208,7 @@ This keeps the decision history honest and traceable.
208
208
  | 0131 | [Rank-scoped skill visibility — feasibility spike (task [#1650](https://example.com/builders#/task/1650)). Wanted: a session lists only the skills the builder's rank can use, via a SessionStart hook filtering the skill menu — pure UX declutter (the server rank gate + each skill's early self-check still enforce). Finding: Claude Code hooks CANNOT filter/hide/rewrite the skill listing — SessionStart offers only `additionalContext` (inject text) + `reloadSkills` (add-only), no hook subtracts a skill, and hooks can't mutate settings at runtime. A HARD menu hide needs a pre-launch step (the launcher that already fetches rank) writing per-builder `skillOverrides:"off"` into `.claude/settings.local.json` before `claude` starts (misses plugin skills; workspace-trust-gated). Recommendation: don't build the heavy launcher change for a cosmetic gain; if some declutter is wanted now, use the existing hook's `additionalContext` as a soft nudge; keep the server gate + self-checks. Owner to decide.](<redacted>.md) | dx / skills |
209
209
  | 0132 | [Migrate the co-hosting fleet to the control plane over an SSH DB tunnel (task [#2071](https://example.com/builders#/task/2071), BONGOS-V1, goal 26; extends ADR 0130, completes 2065 / blocker 64). Stripping the co-hosting box's tokens proved NOT a switch-flip: it runs FIVE token-consuming timers (the 4 `box.js` dev-box lifecycle sweeps + `provision-intent-runner`), each reading its queue from the box's LOCAL `example` Postgres (firewalled to localhost) — so a control-plane runner can't reach the queues, and ADR 0130 moved only box-step execution, not queue reading. Also `box.js` was never migrated. Decision: a persistent SSH tunnel (control-plane `127.0.0.1:5433` → co-hosting `127.0.0.1:5432`, `infra/cohost-db-tunnel.service`) + all five runners re-homed as `cohost-*` control-plane variants reading the example queues via `DATABASE_URL` through the tunnel (no public Postgres exposure), calling DO/CF with the control-plane tokens; provisioning remote-execs box steps per ADR 0130. DB auth: a scram password on the EXISTING `lars` role (identical privileges the runners already use; reachable only via `127.0.0.1`), stored only in chmod-600 `/etc/cloudbongos/cohost-fleet.env`. `box.js` unchanged (pure DO/CF API + Discord/DNS-hook execs, all on the control plane). `cohost-provision-intent-runner.*` supersedes 2065's `provision-intent-runner-cloudbongos.*`. Rejected: exposing Postgres over the VPC (opens a TCP surface + `pg_hba`/cred work; droplets not guaranteed same-VPC); a token-signing broker on the co-hosting box (leaves tokens' blast radius there — opposite of the goal); moving the queue tables to the cloudbongos DB (the OTB web tier can't reach it either — firewall symmetry). Units ship additive + not-enabled, self-healed install-only. Activation (SSH trust · DB password · tunnel · enable `cohost-*` + disable co-hosting timers same window · verify via drift-reconcile · bake · owner token rotate+strip · drop rollback DB) is a sequenced operator-op.](<redacted>.md) | infra / Cloud Bongos |
210
210
  | 0133 | [One-click GitHub sign-in for onboarding via the GitHub App Manifest flow (task [#2080](https://example.com/builders#/task/2080), BONGOS-V1, goal 35 / criterion 165). The last onboarding friction was wiring GitHub sign-in — the owner had to hand-create an OAuth App + paste a client secret, unscriptable because GitHub has NO create-OAuth-App API. Decision: use GitHub's App **Manifest** flow. The signed-in owner clicks one pre-filled link; GitHub creates the App and returns a temporary `code`; the control-plane runner (`provision.js`, sole token-holder) exchanges it for `client_id`/`client_secret`, writes them to `/etc/<slug>/web.env` (the `oauth-secret.js` path), restarts, and verifies `/healthz` auth_configured — the secret never touches the browser, chat, or web tier. Spans the trust boundary: web tier mints a one-time `state` + stores a `provisioning_oauth_manifests` row + a PUBLIC callback stores GitHub's code (`state` is the auth); the control-plane runner drains `code_received` rows on its existing timer tick (gated on instance=active). Sign-in code unchanged — a GitHub App's user token hits the same `GET /user`; GitHub ignores the extra `scope` for Apps. Back-compat: existing OAuth-app instances untouched; manifest is the new default, manual path kept as fallback. All three onboarding surfaces (hall wizard one-click button, `bongos onboard`, `/new-project`) updated in lockstep.](<redacted>.md) | onboarding / auth |
211
- | 0134 | [Private-first npm distribution for the core + client (task [#2089](https://example.com/builders#/task/2089), BONGOS-V1, goal 35 / criterion 165; refines [#2025](https://example.com/builders#/task/2025), builds on ADR 0108). Owner wants the npm/CLI onboarding commands ready but private pre-launch, choosing paid private npm over free GitHub Packages. npm rule forces the shape: unscoped names are always public, only SCOPED packages can be private (paid plan) — so the private form stays scoped `@bongos/core` + `@bongos/client` (zero rename; org already owned). Decision: publish both as scoped PRIVATE packages now via `publishConfig:{access:"restricted"}` baked into the synthesized core `package.json` (`package-core.js buildPackageJson`) + the generated client `package.json` (`gen-api-client.js`); `package-core.js --publish` dry-runs by default, real publish needs `--publish --live` + owner `npm login` (tooling holds no token). Consumers auth via env-fed `.npmrc` `_authToken=<redacted> Vendored-tarball install (task 2053) stays the working default; provision/init registry auto-pull is a follow-up. Going public at launch is one flag (`npm access public`) or task 2025's unscoped vanity `cloudbongos` — 2025 becomes the go-public step, not superseded. The daily `bongos` command is name-independent throughout. Runbook: docs/recipes/private-npm-distribution.md.](<redacted>.md) | distribution / npm |
211
+ | 0134 | [Private-first npm distribution for the core + client (task [#2089](https://example.com/builders#/task/2089), BONGOS-V1, goal 35 / criterion 165; refines [#2025](https://example.com/builders#/task/2025), builds on ADR 0108). Owner wants the npm/CLI onboarding commands ready but private pre-launch, choosing paid private npm over free GitHub Packages. npm rule forces the shape: unscoped names are always public, only SCOPED packages can be private (paid plan) — so the private form stays scoped `@bongos/core` + `@bongos/client` (zero rename; org already owned). Decision: publish both as scoped PRIVATE packages now via `publishConfig:{access:"restricted"}` baked into the synthesized core `package.json` (`package-core.js buildPackageJson`) + the generated client `package.json` (`gen-api-client.js`); `package-core.js --publish` dry-runs by default, real publish needs `--publish --live` + owner `npm login` (tooling holds no token). Consumers auth via env-fed `.npmrc` `_authToken=<redacted> Vendored-tarball install (task 2053) stays the working default; provision/init registry auto-pull is a follow-up. Going public at launch is one flag (`npm access public`) or task 2025's unscoped vanity `cloudbongos` — 2025 becomes the go-public step, not superseded. The daily `bongos` command is name-independent throughout. Runbook: docs/recipes/private-npm-distribution.md. **CLIENT HALF SUPERSEDED** ([task 1003742](https://cloudbongos.com/builders#/task/1003742), 2026-09-09): `@bongos/client` is not an npm package at all — published once at 0.0.2 on 2026-07-07, never updated while the API moved 500+ core versions, and ZERO consumers the whole time (no package.json in the core or any instance repo ever declared it; every consumer vendors a copy, and `@cloudbongos/cli` could not have depended on a private package anyway). Removed from the registry; the generator no longer emits a `publishConfig` and its README documents vendoring. The generated artifact stays (ADR 0118). Core half unaffected.](<redacted>.md) | distribution / npm |
212
212
  | 0135 | [Module upstream submission: the review queue lives in the submitting instance's own DB, interim (task [#1770](https://example.com/builders#/task/1770), BONGOS-V1, builds on ADR 0107). ADR 0107 §1 says `bongos module submit` files a packaged module into a rank-gated review queue "on the core" — but the extracted core (`github.com/example-owner/cloud-bongos`) is a private GIT REPO, not a running service (no HTTP API), and per ADR 0100 §3 each project runs its OWN GDS DB, so there is no shared cross-repo queue to file into; R85 (the forcing-function second consumer) was also abandoned. Decision: the queue is a table (`module_submissions`, migration `<redacted>.sql`) in THIS instance's own Bongos DB. `bongos module submit <key>` and the hall's submit button both call the SAME route, `POST /modules/:key/submit` (metic+archon), which re-runs the ADR 0107 §4 pre-check against its OWN on-disk copy (authoritative — a local CLI pass is a courtesy) and refuses without a sign-off; `GET /modules/submissions` lists the queue for task 1771 (core-side review & accept, still backlog) to read from. Rejected: a live call to a not-yet-built core endpoint (guesses at 1771's contract); a GitHub Issue/PR against the private core repo (loses the structured queue, no standing credential); a dry-run-only local-file client mirroring do-api.js (a worse queue than a DB table this instance already operates). Forward-compatible: when a real core endpoint exists, filing becomes a network hop with no change to the CLI/UI contract.](<redacted>.md) | modules / upstreaming |
213
213
  | 0136 | [Update-channel subscription policy (task [#2150](https://example.com/builders#/task/2150), BONGOS-V1, goal 26; completes the update-consumption workstream on ADR 0100 §2 + ADR 0134 + task 2149). Answers "how do projects subscribe to regular @bongos/core updates?" with the owner-chosen policy: **patch-only by default, health-gated with auto-rollback, opt-in, autonomy-gated.** Per-instance `update_channel` = `pinned` (never) / `patch` (higher z within x.y — DEFAULT) / `minor` (higher y within the major); a MAJOR is never automatic, prereleases never auto-targeted — the pure math is `scripts/gds/update-channel.js` (`resolveChannelTarget`), unit-tested in `tests/update_channel.mjs`. The registry (npm view, ADR 0134) is the "what's newest" source. A new `mode:deterministic` instance-wide routine `core-update-subscription` reads the opt-in roster `config/update-subscriptions.json` (ships EMPTY) and runs `bongos upgrade --to <v> --registry` per subscribed instance with auto-rollback ON (task 2149). Triple-gated: autonomy flag OFF by default (ADR 0115) + empty roster + health-gated. Upstream auto-publish (whether main merges auto-publish a patch) is deliberately SPLIT to its own follow-up task, so goal 26 closes on the consumption half; the subscription just consumes whatever the owner publishes. Scope is local same-host instances; multi-host fleet orchestration is the deferred fleet-control-plane epic (task 1948). Rejected: a `provisioning_instances` DB column (presumes the still-unbuilt fleet-registry schema; a config manifest is migration-free + ADR-0115-shaped), minor-by-default (less conservative), deciding auto-publish here (orthogonal, would block consumption).](<redacted>.md) | distribution / updates |
214
214
  | 0137 | [Upstream publish policy: `@bongos/core` patches are published MANUALLY (owner-gated), NOT auto-published on merge (task [#2158](https://example.com/builders#/task/2158), BONGOS-V1, goal 26; the SUPPLY half split out of ADR 0136 §5 / task 2150, builds on ADR 0134). Owner chose manual over auto. Decision: the owner runs `package-core.js --publish --live` to cut a release (optional `core-v<version>` git tag as the auditable marker — triggers no CI); `CORE_VERSION` (src/module-api.js) is bumped by hand in-task with a changelog line (provenance stays in git, not a release-tool side effect); NO npm write-token at rest (preserves ADR 0134 tokenless tooling — owner auths at publish time; only the read token exists); the no-leak/mirror-redact gate is already fail-closed inside `package-core.js`, so `--publish` cannot upload a leaking artifact. The ADR 0136 subscription consumes whatever is published; freshness is owner-paced by design. Rejected: auto-publish-on-merge (needs a standing npm write-token as a CI secret — reverses the tokenless posture for little gain at low, deliberate release volume). Revisit as a tag-triggered CI publish when the managed-fleet epic (task 1948) makes the manual step a real bottleneck. Ritual: docs/recipes/private-npm-distribution.md "Cutting a core release".](<redacted>.md) | distribution / npm |
@@ -1689,5 +1689,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1689
1689
  landed since 1.19.618 with no explicit bump. run 34320024204. (task 1002620)
1690
1690
  1.19.620 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1691
1691
  landed since 1.19.619 with no explicit bump. run 34321089991. (task 1002620)
1692
+ 1.19.621 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1693
+ landed since 1.19.620 with no explicit bump. run 34321546662. (task 1002620)
1694
+ 1.19.622 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1695
+ landed since 1.19.621 with no explicit bump. run 34322091367. (task 1002620)
1692
1696
  ---------------------------------------------------------------------------
1693
1697
  ```
@@ -1,8 +1,10 @@
1
- # Recipe — private-first npm distribution (core + client)
1
+ # Recipe — private-first npm distribution (the core)
2
2
 
3
3
  > **This is one leg of three.** For the whole path from a change on core `main` to a live instance — and for when publishing to npm is *not* what moves an instance — start at [the core release pipeline](core-release-pipeline.md).
4
4
  >
5
- > Implements [ADR 0134](../adr/<redacted>.md). Pre-launch, `@bongos/core` and `@bongos/client` publish as **private, scoped** npm packages on the owner's paid `bongos` npm org (the canonical brand); at launch they flip to public. The daily command is `bongos` regardless of package name.
5
+ > Implements [ADR 0134](../adr/<redacted>.md), **core half only**. Pre-launch, `@bongos/core` publishes as a **private, scoped** npm package on the owner's paid `bongos` npm org (the canonical brand); at launch it flips to public. The daily command is `bongos` regardless of package name.
6
+ >
7
+ > **`@bongos/client` is no longer part of this.** It was published once (2026-07-07, 0.0.2), never updated while the API moved 500+ core versions, and had **zero** consumers the whole time — every consumer vendors a copy instead. The owner removed it from npm on 2026-09-09; ADR 0134's client half is superseded ([task 1003742](https://cloudbongos.com/builders#/task/1003742)). The generated client still exists and still regenerates — it is simply not a package.
6
8
  >
7
9
  > **Why private must be scoped:** on npm, *unscoped* names are always public — only *scoped* names (`@bongos/…`) can be private, and only on a paid plan. So the packages publish under the `@bongos` scope, which maps to the paid `bongos` org.
8
10
 
@@ -11,9 +13,10 @@
11
13
  | Package | What it is | Built by |
12
14
  |---|---|---|
13
15
  | `@bongos/core` | the platform an instance installs + the `bongos` CLI (`bin`) | `bongos package-core` (`scripts/gds/package-core.js`) — synthesizes the package.json + a pruned lockfile, redacts, and packs a `.tgz` |
14
- | `@bongos/client` | the generated typed API client | `node scripts/gds/gen-api-client.js` (regenerates `clients/bongos-client/`) |
15
16
 
16
- Both carry `publishConfig: { access: "restricted" }`, so **publishing them is private by default** — no public leak if a flag is forgotten.
17
+ `@bongos/core` carries `publishConfig: { access: "restricted" }`, so **publishing it is private by default** — no public leak if a flag is forgotten.
18
+
19
+ The generated typed client (`node scripts/gds/gen-api-client.js` → `clients/bongos-client/`) is deliberately **not** in this table: it is not published, carries no `publishConfig`, and is consumed by vendoring. See its own README.
17
20
 
18
21
  ## One-time owner setup (only the owner can do these)
19
22
 
@@ -80,9 +83,10 @@ Then, with `NPM_TOKEN` set in the environment:
80
83
 
81
84
  ```sh
82
85
  npm install -g @bongos/core # global → the `bongos` command, then `bongos init`
83
- npm install @bongos/client # in a project that calls the API
84
86
  ```
85
87
 
88
+ (A project that calls the API vendors the generated client instead — it is not installable.)
89
+
86
90
  Without a valid token + org membership the install 404s — that is the private gate working.
87
91
 
88
92
  ## Provisioning: registry pull vs vendored tarball (task 2090)
@@ -104,7 +108,6 @@ Pick either — both keep the daily `bongos` command working:
104
108
  - **Flip the same scoped packages public** (simplest):
105
109
  ```sh
106
110
  npm access public @bongos/core
107
- npm access public @bongos/client
108
111
  ```
109
112
  (Or change `publishConfig.access` to `"public"` and republish.) Public installs need no token; the paid seats can be dropped.
110
113
  - **Also publish the unscoped vanity name** for `npm install -g cloudbongos` — that is [task 2025](https://example.com/builders#/task/2025) (rename in `package-core.js`/`upgrade.js` + docs), done at launch.
@@ -855,11 +855,22 @@ function taskTouchesProtectedPath(task) {
855
855
  // and rank them.
856
856
  //
857
857
  // Ranking key (most relevant first):
858
- // 1. priority DESC a P5 in another lane still beats a P1
858
+ // 1. priority ASC, unset last 1 is the MOST urgent. This is the DB's own
859
+ // convention: CHECK (priority BETWEEN 1 AND 5), and every version-progress
860
+ // view weights work as 6 - COALESCE(priority, 5), so a lower number earns
861
+ // more weight. It is the same order the SQL feeds use — `priority ASC
862
+ // NULLS LAST` in the optimizer, `priority NULLS LAST` in listTasks and the
863
+ // goal rollup. A task with no priority set ranks after an explicit P5,
864
+ // never ahead of a P1.
859
865
  // 2. credits-per-minute DESC — best bang-for-effort among equal priority
860
866
  // 3. est_minutes ASC — shorter wins the tiebreak (easier to try)
861
867
  // 4. id ASC — stable final tiebreak
862
868
  //
869
+ // scripts/gds/start.js keeps a hand-copied mirror of this function (it is a
870
+ // pure HTTP client and cannot import this module). The two are a documented
871
+ // mirror pair: tests/cross_discipline_priority_direction.mjs executes BOTH and
872
+ // fails if either drifts. Change one, change the other.
873
+ //
863
874
  // 'unclassified' tasks are excluded: they're un-triaged, not a real lane to
864
875
  // recommend exploring. If preferred_disciplines is empty (no preference), the
865
876
  // builder already sees everything in the main list, so there is nothing
@@ -876,13 +887,22 @@ function rankCrossDisciplineRecommendations(claimable, preferredDisciplines, lim
876
887
  const mins = Number(t.est_minutes_calibrated ?? t.est_minutes) || 0;
877
888
  return mins > 0 ? credits / mins : 0;
878
889
  };
890
+ // `ASC NULLS LAST` expressed in JS: an absent, blank or non-numeric priority
891
+ // sorts AFTER every explicit 1..5. Deliberately not `Number(t.priority) || 0`
892
+ // — that maps NULL to 0, which under an ascending sort is more urgent than a
893
+ // P1. Infinity is the only value that keeps unset work at the back.
894
+ const prio = (t) => {
895
+ if (t.priority === null || t.priority === undefined || t.priority === '') return Infinity;
896
+ const n = Number(t.priority);
897
+ return Number.isFinite(n) ? n : Infinity;
898
+ };
879
899
  const outside = rows.filter(
880
900
  (t) => t.discipline && t.discipline !== 'unclassified' && !prefSet.has(t.discipline)
881
901
  );
882
902
  outside.sort((a, b) => {
883
- const pa = Number(a.priority) || 0;
884
- const pb = Number(b.priority) || 0;
885
- if (pb !== pa) return pb - pa; // higher priority first
903
+ const pa = prio(a);
904
+ const pb = prio(b);
905
+ if (pa !== pb) return pa - pb; // lower number = more urgent; unset last
886
906
  const ca = cpm(a);
887
907
  const cb = cpm(b);
888
908
  if (cb !== ca) return cb - ca; // higher credits/min first
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.620",
3
+ "version": "1.19.622",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.620",
9
+ "version": "1.19.622",
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.620",
3
+ "version": "1.19.622",
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",
@@ -87,11 +87,12 @@ const FILES = [
87
87
  'src/bongos/api-prefix.js',
88
88
 
89
89
  // The generated typed API client (ADR 0118), which cli-lib.js reaches by a dynamic
90
- // `import()` of this exact path. Vendored as a single file rather than depended on: it is
91
- // published as `@bongos/client`, which is PRIVATE, so a public package that declared it as a
92
- // dependency would be uninstallable for everyone. index.mjs is self-contained and imports
93
- // nothing; the sibling package.json is deliberately NOT shipped (it carries
94
- // publishConfig.access=restricted and a nested manifest only confuses packing).
90
+ // `import()` of this exact path. Vendored as a single file rather than depended on: it is not
91
+ // published to npm at all (task 1003742 it was private, had zero consumers, and was removed),
92
+ // so there is nothing to declare a dependency ON. Vendoring was already the right call while it
93
+ // was merely private, since a public package cannot depend on a private one. index.mjs is
94
+ // self-contained and imports nothing; the sibling package.json is deliberately NOT shipped —
95
+ // a nested manifest only confuses packing.
95
96
  'clients/bongos-client/index.mjs',
96
97
 
97
98
  // Neutral branding defaults, so the CLI has sane strings with no instance checkout present.
@@ -10,8 +10,9 @@
10
10
  //
11
11
  // Design (mirrors gen-api-docs.js / ADR 0063): deterministic (stable sort, no
12
12
  // clock), zero runtime deps, --check gate. The client itself has ZERO deps — it
13
- // uses the platform `fetch` (Node 18+ / browsers), so `npm i @bongos/client`
14
- // pulls nothing transitive.
13
+ // uses the platform `fetch` (Node 18+ / browsers), so vendoring it pulls nothing
14
+ // transitive. It is NOT published to npm (task 1003742) — consumers copy the build
15
+ // they need; see the generated README.
15
16
  //
16
17
  // Usage:
17
18
  // node scripts/gds/gen-api-client.js # (re)generate the client package
@@ -372,10 +373,17 @@ function renderPackageJson(spec) {
372
373
  },
373
374
  files: ['index.mjs', 'index.cjs', 'index.d.ts', 'bongos-client.global.js', 'README.md'],
374
375
  license: 'AGPL-3.0-or-later',
375
- // Pre-launch: publish PRIVATE (task 2089 / ADR 0134). Scoped + access:"restricted"
376
- // = only paid-org members can install. Flip to "public" (or `npm access public
377
- // @bongos/client`) at launch, alongside the core.
378
- publishConfig: { access: 'restricted' },
376
+ // NO publishConfig this client is NOT an npm package (task 1003742).
377
+ //
378
+ // It was published once, 2026-07-07 at 0.0.2, under ADR 0134's private-first plan, and never
379
+ // updated while the API it is generated FROM moved 500+ core versions. It had zero consumers
380
+ // the whole time: nothing across the core or any instance repo ever declared it as a
381
+ // dependency — every consumer VENDORS a copy instead (modules/dev-box/app/src/vendor/, and
382
+ // build-cli-package.js into @cloudbongos/cli, which could not depend on it anyway). The owner
383
+ // removed it from the registry on 2026-09-09.
384
+ //
385
+ // The manifest itself stays: `main`/`types`/`exports` are what make the vendored copy and a
386
+ // workspace link resolve. Declaring a publish posture is the part that was fiction.
379
387
  sideEffects: false,
380
388
  }, null, 2) + '\n';
381
389
  }
@@ -427,14 +435,26 @@ function renderReadme(spec, ops) {
427
435
  `- API version: **${spec['x-api-version'] || 'v1'}** (served at \`${base}\`)`,
428
436
  `- ${ops.length} operations across ${new Set(ops.map((o) => o.tag)).size} resource groups`,
429
437
  ``,
430
- `## Install`,
438
+ `## Use it from your project`,
439
+ ``,
440
+ `**This is not an npm package** — it is not published, and nothing depends on it as one`,
441
+ `(task 1003742). Consume it by VENDORING the build you need, which is what every consumer`,
442
+ `in this repo already does:`,
431
443
  ``,
432
444
  '```bash',
433
- `npm install @bongos/client # internal registry / workspace path`,
445
+ `# ESM / bundler: cp clients/bongos-client/index.mjs <your project>/vendor/`,
446
+ `# CommonJS: cp clients/bongos-client/index.cjs <your project>/vendor/`,
447
+ `# Browser global: cp clients/bongos-client/bongos-client.global.js <your project>/public/`,
434
448
  '```',
435
449
  ``,
450
+ `Copy \`index.d.ts\` alongside it for types. Re-copy after \`gen-api-client.js\` runs, so the`,
451
+ `client cannot drift from the routes.`,
452
+ ``,
436
453
  `## Use`,
437
454
  ``,
455
+ `The examples import by package name, which resolves in a workspace. A VENDORED copy is`,
456
+ `imported by its path instead — \`from './vendor/index.mjs'\` — everything below is identical.`,
457
+ ``,
438
458
  '```js',
439
459
  `import { createClient, ApiError } from '@bongos/client';`,
440
460
  ``,
@@ -45,9 +45,9 @@
45
45
  //
46
46
  // Below the main list, a "You might enjoy" section surfaces up to 3 claimable
47
47
  // tasks OUTSIDE the builder's disciplines, ranked by relevance. The ranking
48
- // mirrors db.rankCrossDisciplineRecommendations (the canonical server-side
49
- // home for this logic — kept in sync deliberately; start.js is a pure HTTP
50
- // client and cannot import the server db module).
48
+ // mirrors rankCrossDisciplineRecommendations in modules/lifecycle/db-tasks.js
49
+ // (the canonical server-side home for this logic — kept in sync deliberately;
50
+ // start.js is a pure HTTP client and cannot import the server db module).
51
51
  //
52
52
  //
53
53
  // Returns JSON if --json is passed (for the slash command to parse cleanly).
@@ -470,12 +470,19 @@ function renderWidget({ builder, claims, claimable, total, streak, recommendatio
470
470
  }
471
471
 
472
472
  // Cross-discipline recommendations — mirror of
473
- // db.rankCrossDisciplineRecommendations (src/bongos/db.js). Given the full
474
- // claimable list and the builder's preferred_disciplines, return up to `limit`
475
- // tasks whose discipline is OUTSIDE the builder's preferences, ranked:
476
- // priority DESC, credits/min DESC, est_minutes ASC, id ASC.
473
+ // rankCrossDisciplineRecommendations in modules/lifecycle/db-tasks.js (the
474
+ // canonical copy; it moved there from src/bongos/db.js in the twelve-unit
475
+ // carve). Given the full claimable list and the builder's
476
+ // preferred_disciplines, return up to `limit` tasks whose discipline is OUTSIDE
477
+ // the builder's preferences, ranked:
478
+ // priority ASC (unset last), credits/min DESC, est_minutes ASC, id ASC.
479
+ // Priority 1 is the MOST urgent — the DB's own convention (CHECK (priority
480
+ // BETWEEN 1 AND 5), weighted 6 - COALESCE(priority, 5)); an unset priority
481
+ // sorts after an explicit P5, never ahead of a P1.
477
482
  // 'unclassified' tasks are excluded (un-triaged, not a real lane). Empty
478
483
  // preferences → [] (everything is already in the main list; nothing outside).
484
+ // This copy and the canonical one are pinned together by
485
+ // tests/cross_discipline_priority_direction.mjs — change one, change the other.
479
486
  function rankCrossDisciplineRecommendations(claimable, preferredDisciplines, limit = 3) {
480
487
  const rows = Array.isArray(claimable) ? claimable : [];
481
488
  const prefs = Array.isArray(preferredDisciplines) ? preferredDisciplines : [];
@@ -486,13 +493,21 @@ function rankCrossDisciplineRecommendations(claimable, preferredDisciplines, lim
486
493
  const mins = Number(t.est_minutes_calibrated ?? t.est_minutes) || 0;
487
494
  return mins > 0 ? credits / mins : 0;
488
495
  };
496
+ // `ASC NULLS LAST` in JS — an absent/blank/non-numeric priority sorts after
497
+ // every explicit 1..5. Not `Number(t.priority) || 0`: that maps NULL to 0,
498
+ // which under an ascending sort outranks a P1.
499
+ const prio = (t) => {
500
+ if (t.priority === null || t.priority === undefined || t.priority === '') return Infinity;
501
+ const n = Number(t.priority);
502
+ return Number.isFinite(n) ? n : Infinity;
503
+ };
489
504
  const outside = rows.filter(
490
505
  (t) => t.discipline && t.discipline !== 'unclassified' && !prefSet.has(t.discipline)
491
506
  );
492
507
  outside.sort((a, b) => {
493
- const pa = Number(a.priority) || 0;
494
- const pb = Number(b.priority) || 0;
495
- if (pb !== pa) return pb - pa;
508
+ const pa = prio(a);
509
+ const pb = prio(b);
510
+ if (pa !== pb) return pa - pb;
496
511
  const ca = cpm(a);
497
512
  const cb = cpm(b);
498
513
  if (cb !== ca) return cb - ca;
@@ -973,7 +988,9 @@ async function main() {
973
988
  // Only auto-run when invoked directly (`node start.js …`); a `require()` (the
974
989
  // unit test) imports the pure render helpers without executing the fetch flow —
975
990
  // the claim.js precedent. The owed-rebase warning helpers (ADR 0120 part 5) are
976
- // exported so they can be unit-tested without a live server.
991
+ // exported so they can be unit-tested without a live server, and so is
992
+ // rankCrossDisciplineRecommendations — the mirror test executes THIS copy
993
+ // alongside the canonical one rather than pattern-matching the source.
977
994
  if (require.main === module) {
978
995
  main().catch((err) => {
979
996
  console.error('fatal:', err);
@@ -982,6 +999,7 @@ if (require.main === module) {
982
999
  }
983
1000
 
984
1001
  module.exports = {
1002
+ rankCrossDisciplineRecommendations,
985
1003
  rebaseWarningModel,
986
1004
  rebaseWarningMarkdown,
987
1005
  rebaseWarningWidget,
package/src/module-api.js CHANGED
@@ -55,7 +55,7 @@ const { buildInfo } = require('./build-info');
55
55
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
56
56
  // the entry to that file. Look for a version's history there, not here.
57
57
  // ---------------------------------------------------------------------------
58
- const CORE_VERSION = '1.19.620'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
58
+ const CORE_VERSION = '1.19.622'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
59
59
 
60
60
  // A namespaced logger so a module's log lines are attributable + consistent.
61
61
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,196 @@
1
+ // tests/cross_discipline_priority_direction.mjs — priority sorts ONE way
2
+ // (task 1003756).
3
+ //
4
+ // tasks.priority is 1..5 with 1 the MOST urgent. The DB says so in two places
5
+ // that cannot drift (the CHECK constraint and the 6 - COALESCE(priority, 5)
6
+ // weighting every version-progress view uses), and every SQL feed orders
7
+ // `priority ASC NULLS LAST`. rankCrossDisciplineRecommendations used to sort
8
+ // the other way and its header comment asserted the inverse as the global rule,
9
+ // so re-ranking a task moved it the wrong direction in /builder-start's
10
+ // "You might enjoy" section.
11
+ //
12
+ // The function has TWO hand-copied implementations — the canonical one in
13
+ // modules/lifecycle/db-tasks.js and a mirror in scripts/gds/start.js, which is
14
+ // a pure HTTP client and cannot import the server module. This test EXECUTES
15
+ // both against the same fixtures and asserts they agree, so the pair cannot
16
+ // drift apart again.
17
+ //
18
+ // Run: node tests/cross_discipline_priority_direction.mjs
19
+ import assert from 'node:assert/strict';
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+ import { createRequire } from 'node:module';
24
+ import { makeRunner } from './helpers.mjs';
25
+
26
+ const require = createRequire(import.meta.url);
27
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
28
+ const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
29
+
30
+ const CANONICAL = require(path.join(ROOT, 'modules/lifecycle/db-tasks.js'))
31
+ .rankCrossDisciplineRecommendations;
32
+ const MIRROR = require(path.join(ROOT, 'scripts/gds/start.js'))
33
+ .rankCrossDisciplineRecommendations;
34
+
35
+ // Both copies, run over the same input. Every behavioural assertion below goes
36
+ // through this so a fix applied to only one file fails the suite.
37
+ const IMPLS = [['db-tasks.js (canonical)', CANONICAL], ['start.js (mirror)', MIRROR]];
38
+ const bothRank = (rows, prefs, limit) => IMPLS.map(([name, fn]) => [name, fn(rows, prefs, limit)]);
39
+ const eachImpl = (rows, prefs, limit, check) => {
40
+ for (const [name, out] of bothRank(rows, prefs, limit)) check(out, name);
41
+ };
42
+
43
+ // A claimable row. discipline defaults OUTSIDE the caller's prefs (['engineer'])
44
+ // so the partition step never eats the fixture; credits/minutes are equal by
45
+ // default so priority is the only live sort key unless a test says otherwise.
46
+ const row = (id, priority, extra = {}) => ({
47
+ id, priority, discipline: 'design', credits_reward: 10, est_minutes: 10, ...extra,
48
+ });
49
+ const PREFS = ['engineer'];
50
+ const ids = (out) => out.map((t) => t.id);
51
+
52
+ const { test, summary } = makeRunner();
53
+
54
+ // ---- the direction itself ---------------------------------------------------
55
+
56
+ await test('P1 outranks P5 — 1 is the most urgent, in BOTH copies', () => {
57
+ // Input deliberately in the wrong order so a no-op sort cannot pass.
58
+ const rows = [row('p5', 5), row('p3', 3), row('p1', 1)];
59
+ eachImpl(rows, PREFS, 10, (out, name) => {
60
+ assert.deepEqual(ids(out), ['p1', 'p3', 'p5'],
61
+ `${name}: ascending — the DB weights work 6 - COALESCE(priority, 5), so P1 earns the most`);
62
+ });
63
+ });
64
+
65
+ await test('the limit keeps the MOST urgent, not the least', () => {
66
+ const rows = [row('p5', 5), row('p4', 4), row('p1', 1), row('p2', 2)];
67
+ eachImpl(rows, PREFS, 2, (out, name) => {
68
+ assert.deepEqual(ids(out), ['p1', 'p2'],
69
+ `${name}: slicing after a backwards sort would surface the two LEAST urgent tasks`);
70
+ });
71
+ });
72
+
73
+ // ---- the NULL mapping -------------------------------------------------------
74
+ // The old code read priority as `Number(t.priority) || 0`. Under the old DESC
75
+ // sort that put unset work last by accident; under the corrected ASC sort the
76
+ // same expression would rank it 0 — ahead of every P1. These pin the explicit
77
+ // handling that replaced it.
78
+
79
+ await test('an unset priority sorts LAST, not first (the || 0 trap)', () => {
80
+ for (const blank of [null, undefined, '']) {
81
+ const rows = [row('blank', blank), row('p1', 1), row('p5', 5)];
82
+ eachImpl(rows, PREFS, 10, (out, name) => {
83
+ assert.deepEqual(ids(out), ['p1', 'p5', 'blank'],
84
+ `${name}: priority=${JSON.stringify(blank)} must rank after an explicit P5`);
85
+ });
86
+ }
87
+ });
88
+
89
+ await test('a non-numeric priority is treated as unset, not as 0', () => {
90
+ const rows = [row('junk', 'urgent'), row('p5', 5)];
91
+ eachImpl(rows, PREFS, 10, (out, name) => {
92
+ assert.deepEqual(ids(out), ['p5', 'junk'], `${name}: NaN must not outrank a real priority`);
93
+ });
94
+ });
95
+
96
+ await test('a numeric string priority still sorts as its number', () => {
97
+ const rows = [row('five', '5'), row('one', '1')];
98
+ eachImpl(rows, PREFS, 10, (out, name) => {
99
+ assert.deepEqual(ids(out), ['one', 'five'], `${name}: the API can hand back stringified integers`);
100
+ });
101
+ });
102
+
103
+ // ---- the rest of the ranking key, so fixing direction did not disturb it ----
104
+
105
+ await test('credits-per-minute DESC breaks a priority tie', () => {
106
+ const rows = [
107
+ row('cheap', 2, { credits_reward: 10, est_minutes: 100 }),
108
+ row('rich', 2, { credits_reward: 100, est_minutes: 10 }),
109
+ ];
110
+ eachImpl(rows, PREFS, 10, (out, name) => {
111
+ assert.deepEqual(ids(out), ['rich', 'cheap'], `${name}: best bang-for-effort first`);
112
+ });
113
+ });
114
+
115
+ await test('shorter est_minutes, then id, break the remaining ties', () => {
116
+ const rows = [
117
+ row('long', 2, { credits_reward: 10, est_minutes: 10 }),
118
+ row('short', 2, { credits_reward: 5, est_minutes: 5 }),
119
+ ];
120
+ // Equal credits/min (1.0), so est_minutes decides.
121
+ eachImpl(rows, PREFS, 10, (out, name) => {
122
+ assert.deepEqual(ids(out), ['short', 'long'], `${name}: shorter is easier to try`);
123
+ });
124
+ const tied = [row(9, 2), row(3, 2)];
125
+ eachImpl(tied, PREFS, 10, (out, name) => {
126
+ assert.deepEqual(ids(out), [3, 9], `${name}: id ASC is the stable final tiebreak`);
127
+ });
128
+ });
129
+
130
+ await test('partitioning is unchanged: outside-prefs only, no unclassified, empty prefs → []', () => {
131
+ const rows = [
132
+ row('mine', 1, { discipline: 'engineer' }),
133
+ row('untriaged', 1, { discipline: 'unclassified' }),
134
+ row('theirs', 4, { discipline: 'design' }),
135
+ ];
136
+ eachImpl(rows, PREFS, 10, (out, name) => {
137
+ assert.deepEqual(ids(out), ['theirs'], `${name}: only lanes outside the builder's prefs`);
138
+ });
139
+ eachImpl(rows, [], 10, (out, name) => {
140
+ assert.deepEqual(out, [], `${name}: no stated preference → nothing is "outside"`);
141
+ });
142
+ });
143
+
144
+ await test('the two copies agree row-for-row on a mixed corpus', () => {
145
+ const disciplines = ['design', 'ops', 'research', 'engineer', 'unclassified'];
146
+ const priorities = [1, 2, 3, 4, 5, null, undefined, '', '2', 'junk'];
147
+ const rows = [];
148
+ for (let i = 0; i < 60; i++) {
149
+ rows.push(row(i, priorities[i % priorities.length], {
150
+ discipline: disciplines[i % disciplines.length],
151
+ credits_reward: (i % 7) * 5,
152
+ est_minutes: (i % 4) * 15,
153
+ }));
154
+ }
155
+ const [[, fromCanonical], [, fromMirror]] = bothRank(rows, ['engineer'], 25);
156
+ assert.ok(fromCanonical.length > 5, 'fixture must actually exercise the ranking');
157
+ assert.deepEqual(ids(fromMirror), ids(fromCanonical),
158
+ 'start.js is a hand-copied mirror — it must produce the identical order');
159
+ });
160
+
161
+ // ---- the convention this direction is anchored to ---------------------------
162
+ // If someone ever inverts the DB's meaning of priority, these fail and force the
163
+ // behavioural assertions above to be reconsidered rather than silently re-flipped.
164
+
165
+ await test('the DB still says 1 is most urgent (CHECK + the 6 - COALESCE weighting)', () => {
166
+ const schema = read('migrations/003_pms.sql');
167
+ assert.match(schema, /priority\s+integer\s+CHECK \(priority BETWEEN 1 AND 5\)/,
168
+ 'priority is a 1..5 integer');
169
+ assert.match(schema, /6 - COALESCE\(t\.priority, 5\)/,
170
+ 'progress weighting subtracts from 6, so a LOWER priority number earns MORE weight');
171
+ assert.match(read('src/bongos/optimizer.js'), /priority ASC NULLS LAST/,
172
+ 'the optimizer — the closest analogue to this recommender — orders ascending');
173
+ });
174
+
175
+ await test('neither copy claims priority DESC, and the mirror pointer resolves', () => {
176
+ const canonicalSrc = read('modules/lifecycle/db-tasks.js');
177
+ const mirrorSrc = read('scripts/gds/start.js');
178
+ for (const [name, src] of [['db-tasks.js', canonicalSrc], ['start.js', mirrorSrc]]) {
179
+ assert.ok(!/priority DESC/.test(src), `${name}: no comment may assert priority DESC`);
180
+ }
181
+ // The canonical home moved out of src/bongos/db.js in the twelve-unit carve;
182
+ // start.js pointed at the old address long after the function had left it.
183
+ assert.ok(
184
+ !/rankCrossDisciplineRecommendations \(src\/bongos\/db\.js\)/.test(mirrorSrc)
185
+ && !/db\.rankCrossDisciplineRecommendations/.test(mirrorSrc),
186
+ 'start.js must name modules/lifecycle/db-tasks.js as the canonical home'
187
+ );
188
+ assert.match(mirrorSrc, /modules\/lifecycle\/db-tasks\.js/,
189
+ 'the mirror names where the canonical copy actually lives');
190
+ assert.ok(
191
+ !/rankCrossDisciplineRecommendations/.test(read('src/bongos/db.js')),
192
+ 'src/bongos/db.js really does not define it — the old pointer was dangling'
193
+ );
194
+ });
195
+
196
+ summary();