@bongos/core 1.19.681 → 1.19.682

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,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.681",
6
- "core_contract": "1.19.681",
7
- "source_commit": "e31ae913044e78d0b2b8202995eb551dad418a9e",
5
+ "core_version": "1.19.682",
6
+ "core_contract": "1.19.682",
7
+ "source_commit": "b499e76b5b34ee8e7e753b7b89a3f6e1e8874224",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-12T02:05:45.659Z",
9
+ "built_at": "2026-09-12T16:22:24.694Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 476,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 2624,
20
- "tree_sha256": "f249c4df05688bdb744709b348940982241ccc686471029262564d9b79bdd140",
20
+ "tree_sha256": "a8aca2d995556a722b4debefe87ae388610d584d4954da4285b7dbf0a642eef4",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -322,7 +322,7 @@
322
322
  {
323
323
  "path": ".gitattributes",
324
324
  "mode": "0000644",
325
- "sha256": "72bc38b553e34bc652f0b5b5b2ab24999b4b11c2d6fc3699489e157fd0127a11"
325
+ "sha256": "38bf33dd14a7bff6545f961c4f30172ac850f6a0578900a3c10295c4fc0f27e9"
326
326
  },
327
327
  {
328
328
  "path": "CLAUDE.md",
@@ -2797,7 +2797,7 @@
2797
2797
  {
2798
2798
  "path": "docs/module-api-changelog.md",
2799
2799
  "mode": "0000644",
2800
- "sha256": "244cffb195592c644ad06262ed1a0f610e7744fd62e634a35c5cddd954c9def6"
2800
+ "sha256": "1f62636e946a5b5bbf9b546d2e532b704db15ca21c3a9d0e71f5a8b17cde150b"
2801
2801
  },
2802
2802
  {
2803
2803
  "path": "docs/modules-contract.md",
@@ -5642,7 +5642,7 @@
5642
5642
  {
5643
5643
  "path": "modules/lifecycle/conflict-resolve.js",
5644
5644
  "mode": "0000644",
5645
- "sha256": "1a281a4233b3805550a05ed6bd38b163ab18db658d48675f055b90d2ba722a16"
5645
+ "sha256": "3e20f4965d1d83d17f8e777201bd7e4fd7857843295180542914de16ae51e936"
5646
5646
  },
5647
5647
  {
5648
5648
  "path": "modules/lifecycle/criterion-suggest.js",
@@ -7777,12 +7777,12 @@
7777
7777
  {
7778
7778
  "path": "package-lock.json",
7779
7779
  "mode": "0000644",
7780
- "sha256": "fd92e4204b64289d8404adb924c9142b47c10b0a6c73d709583cbbad01cd6e0a"
7780
+ "sha256": "db81ab75a6e749de67b91e13f3605d79a58a90b2e57c83332f535d48273dabcb"
7781
7781
  },
7782
7782
  {
7783
7783
  "path": "package.json",
7784
7784
  "mode": "0000644",
7785
- "sha256": "242ae038fbdb23f8d375f04e794c87100df9df4c250b93942ca4eaed82017ca3"
7785
+ "sha256": "f4d50516d2a3a9ee031a89735502c1c8ec01789eeb4dfa3fd20da07713679279"
7786
7786
  },
7787
7787
  {
7788
7788
  "path": "public-docs/index.html",
@@ -8282,7 +8282,7 @@
8282
8282
  {
8283
8283
  "path": "scripts/gds/gen-api-client.js",
8284
8284
  "mode": "0000644",
8285
- "sha256": "d363c76caf379a051faea40a2497db8bbba18effd68e9cd9922e9445638c57c5"
8285
+ "sha256": "2d8b35709e19b339bc55c57237f2add36fa6bdeb83a9b036faa902b0c4b8e3f9"
8286
8286
  },
8287
8287
  {
8288
8288
  "path": "scripts/gds/gen-api-docs.js",
@@ -8332,7 +8332,7 @@
8332
8332
  {
8333
8333
  "path": "scripts/gds/git-merge-regen.js",
8334
8334
  "mode": "0000644",
8335
- "sha256": "9896dc201aaa7186838fe4dcecf5226fdb0736b1b7094a98649e2f8fcbfc4d1a"
8335
+ "sha256": "9d65420d85f20d10dba3af63c45d8dd0425ee11dd3d5cfa14d3019abf7354aed"
8336
8336
  },
8337
8337
  {
8338
8338
  "path": "scripts/gds/go-live.js",
@@ -8372,7 +8372,7 @@
8372
8372
  {
8373
8373
  "path": "scripts/gds/init.js",
8374
8374
  "mode": "0000644",
8375
- "sha256": "1d63c1485820ad2e81e97979402eabd3829e86503f4c06318a251dc3a6874047"
8375
+ "sha256": "b0971625852fca93ca474d2f9e31e3601397f1b9efbef3241a0b0c47f17d925f"
8376
8376
  },
8377
8377
  {
8378
8378
  "path": "scripts/gds/install-git-hooks.js",
@@ -9112,7 +9112,7 @@
9112
9112
  {
9113
9113
  "path": "scripts/gds/upgrade.js",
9114
9114
  "mode": "0000644",
9115
- "sha256": "642d0ade55c203d6b07478a07d3c21c26fa4635dc8a92e4fe989434e635abbbb"
9115
+ "sha256": "4f30ee3036846f449f6fc8143da32b3767376352743d373c39ace2f50f66e913"
9116
9116
  },
9117
9117
  {
9118
9118
  "path": "scripts/gds/validate-design.js",
@@ -9142,7 +9142,7 @@
9142
9142
  {
9143
9143
  "path": "scripts/gds/worktree.js",
9144
9144
  "mode": "0000644",
9145
- "sha256": "56549d4c288f55699ceb1fea10f4f65756a9114b4628b3f2b6c44fed0ec6ab26"
9145
+ "sha256": "849cd462b9c50866ca1582ed2156fb68ac76699c8270796d1daf6942c09713dc"
9146
9146
  },
9147
9147
  {
9148
9148
  "path": "scripts/hall-preview/README.md",
@@ -9542,7 +9542,7 @@
9542
9542
  {
9543
9543
  "path": "src/module-api.js",
9544
9544
  "mode": "0000644",
9545
- "sha256": "b06c931aa58a7997f7733e13d2850d290540647c4a7d243cc07d27c20d5b812e"
9545
+ "sha256": "9f875091ba63a70601a61a1d486f5b561026167aa524bd371e457c9a93c2635c"
9546
9546
  },
9547
9547
  {
9548
9548
  "path": "src/module-loader/catalog.js",
@@ -10212,7 +10212,7 @@
10212
10212
  {
10213
10213
  "path": "tests/conflict_resolve.mjs",
10214
10214
  "mode": "0000644",
10215
- "sha256": "e36410c3288ec8c38c22bd5fd285d7abc6e0cadc4433ce87028bb7f9c7d54806"
10215
+ "sha256": "da8182c4235c7735bdee351cfa7dc1146f7ded33b9d594dc0ba498a3b5206d83"
10216
10216
  },
10217
10217
  {
10218
10218
  "path": "tests/connections_api.mjs",
@@ -10642,7 +10642,7 @@
10642
10642
  {
10643
10643
  "path": "tests/git_merge_regen.mjs",
10644
10644
  "mode": "0000644",
10645
- "sha256": "52f02bc7f0da86b29d4b39da78a34f9677f7bdd93e24a4e5365ffb1af64fc413"
10645
+ "sha256": "a55ae255245a53429cc7948e35ea0515bf58307f826d605d6ffd52cc60196ef8"
10646
10646
  },
10647
10647
  {
10648
10648
  "path": "tests/github_push_ancestry.mjs",
@@ -12987,7 +12987,7 @@
12987
12987
  {
12988
12988
  "path": "tests/upgrade.mjs",
12989
12989
  "mode": "0000644",
12990
- "sha256": "106735a9ab28b4a23e2f25269030d982798da3fa39e31d40a1ec78277ac971bc"
12990
+ "sha256": "492b9dc7efdd6f63b81f1662d5fdc9a7541b7f8ccc6c0ec77a456ad3f574ee74"
12991
12991
  },
12992
12992
  {
12993
12993
  "path": "tests/upgrade_persist_pin.mjs",
package/.gitattributes CHANGED
@@ -112,6 +112,34 @@ src/bongos/routes/CLAUDE.md merge=otb-regen
112
112
  docs/copy-registry.json merge=otb-regen
113
113
  docs/copy-inventory.md merge=otb-regen text eol=lf
114
114
 
115
+ # Generated API contract + client (task 1003537). gen-api-docs.js writes the first
116
+ # three of these wholesale from a scan of the route files; gen-api-client.js writes the
117
+ # six client files wholesale from that spec. Nothing is hand-authored in any of the
118
+ # nine, so they are the same whole-file shape as docs/session-log-index.md above — and
119
+ # any branch that adds or changes a route regenerates all nine differently and conflicts
120
+ # on ordinary text. Measured on task 1002334: main took 41 commits in 3 hours and four
121
+ # consecutive merge -> regenerate -> reship cycles each lost the race to a fresh conflict
122
+ # in exactly these files, with a PASSING grade the whole time. REGENERABLE in
123
+ # git-merge-regen.js carries the matching rules, and tests/git_merge_regen.mjs pins the
124
+ # two lists as one set.
125
+ #
126
+ # EVERY PATH IS LISTED EXPLICITLY -- deliberately no `clients/bongos-client/*` glob, for
127
+ # two independent reasons. The lockstep test compares these tokens against REGENERABLE as
128
+ # literal path strings, and the driver is handed git's %P, so a glob would match nothing
129
+ # on either side. And clients/bongos-client/examples/hello-world.mjs is HAND-WRITTEN (the
130
+ # generator owns only the six files below): a glob that captured it would route it through
131
+ # a driver that has no rule for it, forcing a conflict git would otherwise merge cleanly --
132
+ # the same trap the leading slash on /CLAUDE.md above exists to avoid.
133
+ docs/api/openapi.json merge=otb-regen
134
+ docs/api-reference.md merge=otb-regen
135
+ docs/routes-permissions.md merge=otb-regen
136
+ clients/bongos-client/index.mjs merge=otb-regen
137
+ clients/bongos-client/index.cjs merge=otb-regen
138
+ clients/bongos-client/bongos-client.global.js merge=otb-regen
139
+ clients/bongos-client/index.d.ts merge=otb-regen
140
+ clients/bongos-client/package.json merge=otb-regen
141
+ clients/bongos-client/README.md merge=otb-regen
142
+
115
143
  # Append-only index tables (task 1385 / ADR 0082). docs/adr/README.md is a
116
144
  # hand-maintained ADR index whose ONLY recurring conflict is two branches each
117
145
  # appending a new row — there is no generator to regen it from, so `otb-regen`
@@ -1821,5 +1821,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1821
1821
  landed since 1.19.679 with no explicit bump. run 34665216511. (task 1002620)
1822
1822
  1.19.681 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1823
1823
  landed since 1.19.680 with no explicit bump. run 34666648354. (task 1002620)
1824
+ 1.19.682 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1825
+ landed since 1.19.681 with no explicit bump. run 34704993368. (task 1002620)
1824
1826
  ---------------------------------------------------------------------------
1825
1827
  ```
@@ -311,6 +311,19 @@ const GENERATOR_SCRIPTS = [
311
311
  // burned the attempt cap, and announceStrand flagged the task needs_rebase
312
312
  // (PR #536 / task 1003544).
313
313
  'scripts/gds/copy-inventory.js',
314
+ // task 1003537: the generated API contract + client. gen-api-docs.js writes
315
+ // docs/api/openapi.json + docs/api-reference.md + docs/routes-permissions.md from a
316
+ // scan of the route files; gen-api-client.js then writes the six
317
+ // clients/bongos-client files FROM that spec. Both are `--check` gates on the merge
318
+ // ref, so the same trap copy-inventory.js fell into applies verbatim: covered by the
319
+ // merge driver, regenerated by nobody, and the resulting staleness reported as
320
+ // `no_regen_diff` — "a real check failure" — rather than as drift.
321
+ //
322
+ // ORDER IS LOAD-BEARING: gen-api-client.js reads docs/api/openapi.json off disk, so
323
+ // it must run AFTER gen-api-docs.js has rewritten it for the merged tree. This list
324
+ // is walked in order, one await at a time, which is what makes that safe.
325
+ 'scripts/gds/gen-api-docs.js',
326
+ 'scripts/gds/gen-api-client.js',
314
327
  ];
315
328
 
316
329
  // Run the CLONE's generator scripts in WRITE mode against the merged working tree.
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.681",
3
+ "version": "1.19.682",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.681",
9
+ "version": "1.19.682",
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.681",
3
+ "version": "1.19.682",
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",
@@ -487,8 +487,13 @@ function renderReadme(spec, ops) {
487
487
  ].join('\n');
488
488
  }
489
489
 
490
- function build() {
491
- const spec = JSON.parse(fs.readFileSync(SPEC_PATH, 'utf8'));
490
+ // injectedSpec (task 1003537): the otb-regen merge driver regenerates this client
491
+ // DURING a merge, when docs/api/openapi.json on disk may be the conflicted file git has
492
+ // just written markers into — JSON.parse would throw on it. Passing a spec built in
493
+ // memory keeps the heal working and keeps the client consistent with the api-reference.md
494
+ // the same merge resolves. Omitted everywhere else, so the CLI path is unchanged.
495
+ function build(injectedSpec) {
496
+ const spec = injectedSpec || JSON.parse(fs.readFileSync(SPEC_PATH, 'utf8'));
492
497
  const ops = buildModel(spec);
493
498
  const esm = renderClientJs(spec, ops);
494
499
  return {
@@ -54,9 +54,40 @@ const G = require('./gen-repo-map');
54
54
  const FM = require('./gen-file-map');
55
55
  const SI = require('./gen-session-index');
56
56
  const CI = require('./copy-inventory');
57
+ // gen-api-docs / gen-api-client are required LAZILY, inside the two rules that use them —
58
+ // deliberately NOT here (task 1003537). gen-api-docs pulls src/bongos/routes/_helpers.js
59
+ // for its LIMITS caps, and ship.js requires this module, so an eager require put a server
60
+ // route file into the boot of every `bongos` subcommand. tests/module_api_lazy.mjs catches
61
+ // exactly that. require() is cached, so the merge path pays the load once and the CLI
62
+ // path never pays it at all.
57
63
 
58
64
  const REPO_ROOT = path.resolve(__dirname, '..', '..');
59
65
 
66
+ // The generated API contract (task 1003537). gen-api-docs.js writes all three of these
67
+ // wholesale from a scan of the route files, so one build() call answers whichever of
68
+ // them git hands us.
69
+ const API_DOC_FILES = new Set([
70
+ 'docs/api/openapi.json',
71
+ 'docs/api-reference.md',
72
+ 'docs/routes-permissions.md',
73
+ ]);
74
+
75
+ // The generated client (task 1003537), written wholesale from the spec by
76
+ // gen-api-client.js. These six names mirror the keys of its build().files; the
77
+ // lockstep is asserted in tests/git_merge_regen.mjs rather than derived here, because
78
+ // deriving it would mean running the generator at require time. NOTE what is absent:
79
+ // clients/bongos-client/examples/hello-world.mjs is hand-written and must stay out of
80
+ // this set, or the driver would force a conflict on a file git can merge by itself.
81
+ const API_CLIENT_DIR = 'clients/bongos-client';
82
+ const API_CLIENT_FILES = new Set([
83
+ 'index.mjs',
84
+ 'index.cjs',
85
+ 'bongos-client.global.js',
86
+ 'index.d.ts',
87
+ 'package.json',
88
+ 'README.md',
89
+ ].map((n) => `${API_CLIENT_DIR}/${n}`));
90
+
60
91
  // The files this driver knows how to regenerate. docs/repo-map.md and
61
92
  // docs/session-log-index.md are whole-file; each NESTED_DIRS CLAUDE.md is
62
93
  // hand-written prose with one generated block; docs/file-map.md is hand-written
@@ -73,6 +104,10 @@ const REPO_ROOT = path.resolve(__dirname, '..', '..');
73
104
  // that each add a session-log file regenerate that block differently and ALWAYS conflict
74
105
  // there, and nothing healed it: the driver did not know the file, and `merge=union` would be
75
106
  // wrong for prose. Same shape as docs/file-map.md (3-way-merge the prose, re-render the block).
107
+ // task 1003537: the two API sets above are spread in whole — every one of those nine files
108
+ // is generated end to end, so they take the docs/repo-map.md path (ignore all three merge
109
+ // sides, rebuild). They are named up there rather than inline here because the rules below
110
+ // test set membership, and tests/git_merge_regen.mjs cross-checks them against .gitattributes.
76
111
  const REGENERABLE = new Set([
77
112
  'CLAUDE.md',
78
113
  'docs/repo-map.md',
@@ -81,6 +116,8 @@ const REGENERABLE = new Set([
81
116
  'docs/copy-inventory.md',
82
117
  'docs/copy-registry.json',
83
118
  ...G.NESTED_DIRS.map((d) => `${d}/CLAUDE.md`),
119
+ ...API_DOC_FILES,
120
+ ...API_CLIENT_FILES,
84
121
  ]);
85
122
 
86
123
  // A line that begins with a git conflict marker. If any survives our resolution,
@@ -137,6 +174,33 @@ function regenerateResolved(relpath, { base = '', ours = '', theirs = '' } = {})
137
174
  return { content: CI.renderReport(CI.buildRegistry()), ok: true };
138
175
  }
139
176
 
177
+ // The generated API contract (task 1003537): gen-api-docs.js writes openapi.json,
178
+ // api-reference.md and routes-permissions.md wholesale from a scan of the route files
179
+ // — nothing hand-authored in any of them — so like docs/repo-map.md below they ignore
180
+ // all three merge sides and rebuild. One build() yields all three; take the one asked
181
+ // for. This is the rule that ends the task-1002334 stall, where four merge → reship
182
+ // cycles each lost the race to a fresh textual conflict in exactly these files.
183
+ if (API_DOC_FILES.has(relpath)) {
184
+ const AD = require('./gen-api-docs'); // lazy — see the require block up top
185
+ const f = AD.build().files.find((x) => x.rel === relpath);
186
+ return { content: f.content, ok: true };
187
+ }
188
+
189
+ // The generated client (task 1003537): gen-api-client.js renders all six files from
190
+ // the spec, so it heals the same way. Hand it a FRESHLY BUILT spec instead of letting
191
+ // build() read docs/api/openapi.json off disk — mid-merge that file is very often the
192
+ // one git has just written conflict markers into, and JSON.parse would throw on them
193
+ // (an uncaught throw here exits non-zero, which git reads as "unresolved", so the
194
+ // failure would be a silent fallback to a textual conflict on the very files this
195
+ // rule exists to heal). Rebuilding also keeps the client consistent with the
196
+ // api-reference.md the same merge is healing.
197
+ if (API_CLIENT_FILES.has(relpath)) {
198
+ const AD = require('./gen-api-docs'); // lazy — see the require block up top
199
+ const AC = require('./gen-api-client'); // lazy — same reason
200
+ const { files } = AC.build(AD.buildOpenapi(AD.extractModel()));
201
+ return { content: files[relpath.slice(API_CLIENT_DIR.length + 1)], ok: true };
202
+ }
203
+
140
204
  // The ROOT CLAUDE.md (task 1001431): hand-written prose carrying ONE generated block, the
141
205
  // §13 session-log snippet. 3-way-merge the prose, then re-render the snippet from the
142
206
  // session-log files on disk. Must be handled BEFORE the nested-CLAUDE.md fallthrough below,
@@ -943,7 +943,7 @@ async function main(argv) {
943
943
  console.error(`init: could not derive the core version from ${path.basename(tgz)} — expected a tarball named ${upgrade.ARTIFACT}-<version>.tgz (produced by \`node scripts/gds/package-core.js\`).`);
944
944
  return 1;
945
945
  }
946
- const vendorRel = path.join('vendor', `${upgrade.ARTIFACT}-${version}.tgz`);
946
+ const vendorRel = upgrade.vendorRelFor(version);
947
947
  coreDep = `file:${vendorRel}`;
948
948
  vendorPlan = { tgz, version, vendorRel };
949
949
  }
@@ -34,6 +34,16 @@ const { writeNpmrc, hasNpmToken, resolveNpmToken } = require('./npmrc'); // ADR
34
34
 
35
35
  const CORE_PKG = '@bongos/core';
36
36
  const ARTIFACT = 'bongos-core';
37
+ // WHERE THE VENDORED TARBALL SITS, as a package.json `file:` specifier — and the reason
38
+ // this is not a path.join(). The same string is spent two ways: it is joined onto an
39
+ // absolute dir for an fs check (path.join normalises a forward slash on every platform,
40
+ // so that direction is free), and it is WRITTEN INTO package.json as `file:<vendorRel>`.
41
+ // npm specifiers are POSIX. A path.join() here emits `file:vendor\\bongos-core-x.y.z.tgz`
42
+ // on a Windows builder's machine, and that pin is then COMMITTED to the instance repo —
43
+ // where the provisioner runs `npm ci` on Linux and reads a literal backslash in the
44
+ // filename. One copy, POSIX by construction, so a scaffold cannot be platform-stamped
45
+ // by the box that happened to run the scaffolder (task 1003839).
46
+ function vendorRelFor(version) { return path.posix.join('vendor', `${ARTIFACT}-${version}.tgz`); }
37
47
 
38
48
  // ---- pure helpers (no I/O side effects beyond the injected fs) --------------
39
49
 
@@ -245,7 +255,7 @@ function vendorTarball({ instanceDir, fromTgz, targetVersion }, fsImpl = fs) {
245
255
  if (fsImpl.existsSync(fromManifest)) {
246
256
  fsImpl.copyFileSync(fromManifest, path.join(vendorDir, `${ARTIFACT}-${targetVersion}.manifest.json`));
247
257
  }
248
- return path.join('vendor', destName);
258
+ return path.posix.join('vendor', destName);
249
259
  }
250
260
 
251
261
  // Lightweight module-compat pre-check reusing module.js against the TARGET core version. Best-effort:
@@ -940,10 +950,10 @@ async function runUpgrade(opts, deps = {}) {
940
950
  if (!dryRun) { writeNpmrc(instanceDir, { fsImpl, overwrite: false, log }); snapshot = { prevPin, prevVersion: fromVersion }; }
941
951
  log(` ${dryRun ? '[dry-run] would pin' : '✓ pinned'} ${CORE_PKG} → ${targetVersion} (registry)${dryRun ? ' + write an env-fed .npmrc' : ''}${prevPin ? ` (was ${prevPin})` : ''}`);
942
952
  } else {
943
- let vendorRel = opts.pinPath || (opts.from ? null : path.join('vendor', `${ARTIFACT}-${targetVersion}.tgz`));
953
+ let vendorRel = opts.pinPath || (opts.from ? null : vendorRelFor(targetVersion));
944
954
  if (opts.from) {
945
955
  if (!fsImpl.existsSync(opts.from)) return { ok: false, error: `--from tarball not found: ${opts.from}` };
946
- if (dryRun) { log(` [dry-run] would vendor ${opts.from} → vendor/${ARTIFACT}-${targetVersion}.tgz`); vendorRel = path.join('vendor', `${ARTIFACT}-${targetVersion}.tgz`); }
956
+ if (dryRun) { log(` [dry-run] would vendor ${opts.from} → vendor/${ARTIFACT}-${targetVersion}.tgz`); vendorRel = vendorRelFor(targetVersion); }
947
957
  else vendorRel = vendorTarball({ instanceDir, fromTgz: path.resolve(opts.from), targetVersion }, fsImpl);
948
958
  }
949
959
  if (!fsImpl.existsSync(path.join(instanceDir, vendorRel)) && !dryRun) {
@@ -1225,7 +1235,7 @@ module.exports = {
1225
1235
  versionFromTgzPath, versionFromDep, readInstalledCoreVersion, readPinnedCoreVersion, writePin, writeRegistryPin, restorePin,
1226
1236
  vendorTarball, gitTreeClean, persistPin, dirtyPinFiles, gitCurrentBranch, PIN_FILES, preflightDbIdentity, resolveInstanceDb, preflightModules, reportModulePreflight, npmInstall, runMigrate, regenerateApiArtifacts, regenerateNavDocs, NAV_WHOLE_FILE_GENERATORS, missingDocAssets, healDocAssets, DOC_ASSET_GENERATORS, restartService, recordLedger, pollHealth, deriveVersionUrl, pollServedVersion,
1227
1237
  readManifest, resolveReferenceManifest, verifyInstalledPin, // task 1002216 (audit H8) — integrity-pin verification
1228
- CORE_PKG, ARTIFACT,
1238
+ CORE_PKG, ARTIFACT, vendorRelFor,
1229
1239
  };
1230
1240
 
1231
1241
  if (require.main === module) {
@@ -31,6 +31,7 @@ const fs = require('node:fs');
31
31
  const path = require('node:path');
32
32
  const { execFileSync } = require('node:child_process');
33
33
  const { arg, hasFlag } = require('./cli-lib');
34
+ const { registerMergeDriver, MERGE_DRIVER_NAME } = require('./install-git-hooks');
34
35
 
35
36
  // -- pure helpers (exported for tests/worktree_helper.mjs) --------------------
36
37
 
@@ -145,6 +146,25 @@ function cmdAdd(root, argv) {
145
146
  process.exit(1);
146
147
  }
147
148
 
149
+ // Register the otb-regen merge driver (task 1003537). .gitattributes routes the
150
+ // generated files through it, but git honours that ONLY where merge.otb-regen.driver is
151
+ // configured — which install-git-hooks.js does per CLONE, on /builder-setup. A builder
152
+ // who never ran the installer got a silent TEXTUAL merge of files no textual merge can
153
+ // resolve: measured on task 1002334, a scripts/gds/CLAUDE.md symbol block matching
154
+ // neither generator, clean enough that nothing flagged it until fitness ran in CI.
155
+ // Worktree-per-claim is the default path to a new tree, so doing it here is what closes
156
+ // that gap for a clone whose owner never ran setup. git config is shared with every
157
+ // linked worktree, so registering against the main root covers this tree and all later
158
+ // ones. Idempotent, and best-effort on purpose: failing to register only restores the
159
+ // pre-existing fallback-to-conflict behaviour, which must never fail `worktree add`.
160
+ const driver = registerMergeDriver(root);
161
+ if (driver.ok) {
162
+ console.log(` merge driver '${MERGE_DRIVER_NAME}' registered (generated files auto-regenerate on merge)`);
163
+ } else {
164
+ console.log(` ⚠ could not register the '${MERGE_DRIVER_NAME}' merge driver: ${driver.error}`);
165
+ console.log(' (generated files will merge textually here; ship.js still self-heals the same set)');
166
+ }
167
+
148
168
  if (needsJunction({ wtPath, repoRoot: root, platform: process.platform })) {
149
169
  const target = path.join(root, 'node_modules');
150
170
  if (!fs.existsSync(target)) {
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.681'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.682'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -562,6 +562,19 @@ test('1003546 every REGENERABLE artifact has a producer, and every producer is i
562
562
  [CI.REGISTRY_REL, 'scripts/gds/copy-inventory.js'],
563
563
  [CI.REPORT_REL, 'scripts/gds/copy-inventory.js'],
564
564
  ...G.NESTED_DIRS.map((d) => [`${d}/CLAUDE.md`, 'scripts/gds/gen-repo-map.js']),
565
+ // task 1003537 — the generated API contract + client joined REGENERABLE. Mapped by
566
+ // PATH PREFIX rather than by calling the generators: this suite deliberately sets
567
+ // *_INSTANCE_ROOT (see the env-hardening case above), which repoints their docs root
568
+ // at a fake server checkout, so a build() here reads a file that does not exist.
569
+ // Nothing is lost by not running them — drift between REGENERABLE and what they
570
+ // actually write is pinned by tests/git_merge_regen.mjs case 11, and a future entry
571
+ // under either prefix is picked up here automatically.
572
+ ...[...regen.REGENERABLE]
573
+ .filter((p) => p.startsWith('docs/api/') || p === 'docs/api-reference.md' || p === 'docs/routes-permissions.md')
574
+ .map((p) => [p, 'scripts/gds/gen-api-docs.js']),
575
+ ...[...regen.REGENERABLE]
576
+ .filter((p) => p.startsWith('clients/bongos-client/'))
577
+ .map((p) => [p, 'scripts/gds/gen-api-client.js']),
565
578
  ]);
566
579
 
567
580
  for (const p of regen.REGENERABLE) {
@@ -28,6 +28,12 @@
28
28
  // the merge's OWNER must regenerate afterwards (conflict-resolve.js step 7b).
29
29
  // Needs a real clone (the driver's __dirname must BE the merging tree), so it
30
30
  // skips where the environment cannot host that.
31
+ // 11. The generated API contract + client (task 1003537) — docs/api/openapi.json,
32
+ // docs/api-reference.md, docs/routes-permissions.md and the six
33
+ // clients/bongos-client files regenerate clean from the merged tree, the client
34
+ // heals even when the on-disk spec is the conflicted file, and the driver's file
35
+ // list still matches what gen-api-client actually writes. Also pins the ABSENCE of
36
+ // clients/bongos-client/examples/hello-world.mjs, which is hand-written.
31
37
  //
32
38
  // Run: node tests/git_merge_regen.mjs
33
39
 
@@ -46,6 +52,8 @@ const regen = require('../scripts/gds/git-merge-regen.js');
46
52
  const G = require('../scripts/gds/gen-repo-map.js');
47
53
  const CI = require('../scripts/gds/copy-inventory.js');
48
54
  const SI = require('../scripts/gds/gen-session-index.js'); // task 1001431 — the root CLAUDE.md's §13 snippet
55
+ const AD = require('../scripts/gds/gen-api-docs.js'); // task 1003537 — the generated API contract
56
+ const AC = require('../scripts/gds/gen-api-client.js'); // task 1003537 — the generated client
49
57
 
50
58
  const MARKERS = /^(<{7}|={7}|>{7})/m;
51
59
 
@@ -438,4 +446,56 @@ function withBlockBody(src, body) {
438
446
  }
439
447
  }
440
448
 
449
+ // ── 11) the generated API contract + client (task 1003537) ─────────────────────
450
+ // gen-api-docs.js and gen-api-client.js write nine fully generated files that used to
451
+ // merge textually, so any branch touching a route conflicted with every concurrent land
452
+ // (task 1002334: four merge → reship cycles, each beaten by a fresh conflict). They heal
453
+ // like docs/repo-map.md — all three merge sides ignored, rebuilt from the merged tree.
454
+ {
455
+ const conflicted = (a, b) => `<<<<<<< ours\n${a}\n=======\n${b}\n>>>>>>> theirs\n`;
456
+
457
+ // (a) the three doc artifacts rebuild to exactly what the generator would write.
458
+ const fresh = new Map(AD.build().files.map((f) => [f.rel, f.content]));
459
+ for (const rel of ['docs/api/openapi.json', 'docs/api-reference.md', 'docs/routes-permissions.md']) {
460
+ const r = regen.regenerateResolved(rel, {
461
+ base: 'base\n', ours: conflicted('{"a":1}', '{"b":2}'), theirs: 'nonsense\n',
462
+ });
463
+ assert.equal(r.ok, true, `${rel} heal must succeed`);
464
+ assert.ok(!MARKERS.test(r.content), `${rel} heal must leave no conflict markers`);
465
+ assert.equal(r.content, fresh.get(rel), `${rel} heal must equal a fresh generation`);
466
+ }
467
+
468
+ // (b) the six client files do too. The driver rebuilds the spec in memory rather than
469
+ // reading docs/api/openapi.json, so this must hold no matter what is on disk.
470
+ const freshClient = AC.build().files;
471
+ for (const [name, content] of Object.entries(freshClient)) {
472
+ const rel = `clients/bongos-client/${name}`;
473
+ const r = regen.regenerateResolved(rel, {
474
+ base: 'base\n', ours: conflicted('one', 'two'), theirs: 'nonsense\n',
475
+ });
476
+ assert.equal(r.ok, true, `${rel} heal must succeed`);
477
+ assert.ok(!MARKERS.test(r.content), `${rel} heal must leave no conflict markers`);
478
+ assert.equal(r.content, content, `${rel} heal must equal a fresh generation`);
479
+ }
480
+
481
+ // (c) LOCKSTEP: the driver's hardcoded client list must be exactly what the generator
482
+ // writes. Without this, a seventh generated client file would be added to
483
+ // gen-api-client.js and silently fall back to a textual merge — the original defect.
484
+ const declared = [...regen.REGENERABLE]
485
+ .filter((f) => f.startsWith('clients/bongos-client/'))
486
+ .map((f) => f.slice('clients/bongos-client/'.length))
487
+ .sort();
488
+ assert.deepEqual(declared, Object.keys(freshClient).sort(),
489
+ 'REGENERABLE\'s clients/bongos-client entries must match gen-api-client build().files exactly');
490
+
491
+ // (d) the hand-written example must stay OUT. A `clients/bongos-client/*` glob would
492
+ // have swept it in, and the driver has no rule for it — so git would be forced into a
493
+ // conflict on a file it can merge by itself (the /CLAUDE.md leading-slash trap again).
494
+ assert.ok(!regen.REGENERABLE.has('clients/bongos-client/examples/hello-world.mjs'),
495
+ 'the hand-written example must never be routed through the regenerating driver');
496
+ assert.equal(regen.regenerateResolved('clients/bongos-client/examples/hello-world.mjs',
497
+ { base: 'a\n', ours: 'b\n', theirs: 'c\n' }).content, null,
498
+ 'a non-generated path under the client dir must not resolve');
499
+ }
500
+
441
501
  console.log('git_merge_regen: all assertions passed');
package/tests/upgrade.mjs CHANGED
@@ -88,11 +88,51 @@ t('vendorTarball: copies the .tgz + side-car manifest under the canonical name',
88
88
  writeFileSync(join(src, 'bongos-core-1.15.0.manifest.json'), '{"v":1}');
89
89
  const dir = scratchConsumer();
90
90
  const rel = u.vendorTarball({ instanceDir: dir, fromTgz: join(src, 'bongos-core-1.15.0.tgz'), targetVersion: '1.15.0' });
91
- assert.equal(rel, join('vendor', 'bongos-core-1.15.0.tgz'));
91
+ // POSIX outright, not join(): this return value is written into package.json as a
92
+ // `file:` specifier, so the separator is part of the contract, not of the platform
93
+ assert.equal(rel, 'vendor/bongos-core-1.15.0.tgz');
92
94
  assert.equal(readFileSync(join(dir, rel), 'utf8'), 'TGZBYTES');
93
95
  assert.ok(existsSync(join(dir, 'vendor', 'bongos-core-1.15.0.manifest.json')), 'manifest copied too');
94
96
  });
95
97
 
98
+ // The pin is a POSIX specifier on EVERY platform (task 1003839). `bongos init
99
+ // --vendor-core` and `bongos upgrade` both write `file:<vendorRel>` into an
100
+ // instance's package.json, and that package.json is COMMITTED — so a scaffold cut
101
+ // on a Windows laptop was shipping `file:vendor\\bongos-core-x.y.z.tgz` to a Linux
102
+ // provisioner running `npm ci`, which reads the backslash as part of the filename.
103
+ //
104
+ // Two halves, because neither alone can see the bug from CI. The STATIC half reads
105
+ // the sources and so fails on any platform; the BEHAVIOURAL half below it is silent on
106
+ // Linux (path.join already yields a forward slash there) and loud on Windows. The
107
+ // static half runs FIRST for exactly that reason: on Windows a regression trips the
108
+ // behavioural assertions and would abort the case before the platform-independent
109
+ // ones ever ran, hiding whether CI could have caught it.
110
+ t('the vendored core pin is POSIX on every platform, and no call site rebuilds it', () => {
111
+ // one place decides the separator, and that place spells it posix
112
+ const upgradeSrc = readFileSync(new URL('../scripts/gds/upgrade.js', import.meta.url), 'utf8');
113
+ assert.match(upgradeSrc, /function vendorRelFor\([^)]*\)\s*{\s*return path\.posix\.join\(/,
114
+ 'vendorRelFor must build the pin with path.posix.join — it is an npm specifier, not a filesystem path');
115
+ for (const rel of ['scripts/gds/upgrade.js', 'scripts/gds/init.js']) {
116
+ const code = readFileSync(new URL('../' + rel, import.meta.url), 'utf8');
117
+ assert.doesNotMatch(code, /path\.join\(\s*['"]vendor['"]\s*,/,
118
+ `${rel} must build the pin with vendorRelFor(), not path.join('vendor', …)`);
119
+ }
120
+
121
+ // and the string it produces, end to end
122
+ assert.equal(u.vendorRelFor('1.17.0'), 'vendor/bongos-core-1.17.0.tgz');
123
+ assert.doesNotMatch(u.vendorRelFor('1.17.0'), /\\/, 'a package.json file: specifier carries no backslash');
124
+ // still a usable path: join() normalises a forward-slash relative on Windows too,
125
+ // so POSIX costs the fs side nothing
126
+ const dir = scratchConsumer();
127
+ mkdirSync(join(dir, 'vendor'), { recursive: true });
128
+ writeFileSync(join(dir, u.vendorRelFor('1.17.0')), 'TGZ');
129
+ assert.ok(existsSync(join(dir, u.vendorRelFor('1.17.0'))), 'the POSIX rel still resolves on disk');
130
+ // and the pin that reaches package.json is that string, verbatim
131
+ u.writePin(dir, u.vendorRelFor('1.17.0'));
132
+ assert.equal(JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).dependencies[u.CORE_PKG],
133
+ 'file:vendor/bongos-core-1.17.0.tgz');
134
+ });
135
+
96
136
  // ---- 3. step shims issue the right argv ------------------------------------
97
137
  t('npmInstall / runMigrate / restartService: correct commands', () => {
98
138
  const calls = [];