@bongos/core 1.19.680 → 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 +24 -24
- package/.gitattributes +44 -2
- package/clients/bongos-client/index.d.ts +1 -2
- package/docs/api/openapi.json +3 -29
- package/docs/module-api-changelog.md +4 -0
- package/modules/lifecycle/conflict-resolve.js +13 -0
- package/modules/lifecycle/github-push.js +23 -5
- package/modules/lifecycle/routes/tasks.js +36 -8
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/gen-api-client.js +7 -2
- package/scripts/gds/git-merge-regen.js +93 -0
- package/scripts/gds/init.js +1 -1
- package/scripts/gds/upgrade.js +14 -4
- package/scripts/gds/worktree.js +20 -0
- package/src/module-api.js +1 -1
- package/tests/conflict_resolve.mjs +18 -0
- package/tests/git_merge_regen.mjs +124 -2
- package/tests/publish_branch_route.mjs +57 -0
- package/tests/upgrade.mjs +41 -1
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
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-
|
|
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": "
|
|
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": "
|
|
325
|
+
"sha256": "38bf33dd14a7bff6545f961c4f30172ac850f6a0578900a3c10295c4fc0f27e9"
|
|
326
326
|
},
|
|
327
327
|
{
|
|
328
328
|
"path": "CLAUDE.md",
|
|
@@ -387,7 +387,7 @@
|
|
|
387
387
|
{
|
|
388
388
|
"path": "clients/bongos-client/index.d.ts",
|
|
389
389
|
"mode": "0000644",
|
|
390
|
-
"sha256": "
|
|
390
|
+
"sha256": "101095af6616b30d3338ec12394343f360d8a583ef3da84f0dce49edc80630cb"
|
|
391
391
|
},
|
|
392
392
|
{
|
|
393
393
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -1922,7 +1922,7 @@
|
|
|
1922
1922
|
{
|
|
1923
1923
|
"path": "docs/api/openapi.json",
|
|
1924
1924
|
"mode": "0000644",
|
|
1925
|
-
"sha256": "
|
|
1925
|
+
"sha256": "b09110e4eec5225c0594e4f7a3bd3e18878f751ee4697af87209568d69483c3a"
|
|
1926
1926
|
},
|
|
1927
1927
|
{
|
|
1928
1928
|
"path": "docs/architecture.md",
|
|
@@ -2797,7 +2797,7 @@
|
|
|
2797
2797
|
{
|
|
2798
2798
|
"path": "docs/module-api-changelog.md",
|
|
2799
2799
|
"mode": "0000644",
|
|
2800
|
-
"sha256": "
|
|
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": "
|
|
5645
|
+
"sha256": "3e20f4965d1d83d17f8e777201bd7e4fd7857843295180542914de16ae51e936"
|
|
5646
5646
|
},
|
|
5647
5647
|
{
|
|
5648
5648
|
"path": "modules/lifecycle/criterion-suggest.js",
|
|
@@ -5732,7 +5732,7 @@
|
|
|
5732
5732
|
{
|
|
5733
5733
|
"path": "modules/lifecycle/github-push.js",
|
|
5734
5734
|
"mode": "0000644",
|
|
5735
|
-
"sha256": "
|
|
5735
|
+
"sha256": "1a84e2225c58969b7bc6c43643921278966c78a6049488deb6c39048982ae005"
|
|
5736
5736
|
},
|
|
5737
5737
|
{
|
|
5738
5738
|
"path": "modules/lifecycle/goal-advisory.js",
|
|
@@ -5947,7 +5947,7 @@
|
|
|
5947
5947
|
{
|
|
5948
5948
|
"path": "modules/lifecycle/routes/tasks.js",
|
|
5949
5949
|
"mode": "0000644",
|
|
5950
|
-
"sha256": "
|
|
5950
|
+
"sha256": "456b1d53d2349a499e1b07897dc6e16c72d26156a71347382c5a2508dc94d28d"
|
|
5951
5951
|
},
|
|
5952
5952
|
{
|
|
5953
5953
|
"path": "modules/lifecycle/routes/version-route-authz.js",
|
|
@@ -7777,12 +7777,12 @@
|
|
|
7777
7777
|
{
|
|
7778
7778
|
"path": "package-lock.json",
|
|
7779
7779
|
"mode": "0000644",
|
|
7780
|
-
"sha256": "
|
|
7780
|
+
"sha256": "db81ab75a6e749de67b91e13f3605d79a58a90b2e57c83332f535d48273dabcb"
|
|
7781
7781
|
},
|
|
7782
7782
|
{
|
|
7783
7783
|
"path": "package.json",
|
|
7784
7784
|
"mode": "0000644",
|
|
7785
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
10645
|
+
"sha256": "a55ae255245a53429cc7948e35ea0515bf58307f826d605d6ffd52cc60196ef8"
|
|
10646
10646
|
},
|
|
10647
10647
|
{
|
|
10648
10648
|
"path": "tests/github_push_ancestry.mjs",
|
|
@@ -12192,7 +12192,7 @@
|
|
|
12192
12192
|
{
|
|
12193
12193
|
"path": "tests/publish_branch_route.mjs",
|
|
12194
12194
|
"mode": "0000644",
|
|
12195
|
-
"sha256": "
|
|
12195
|
+
"sha256": "f688f5da9bfc392ce203ee3b89dfa9ba23fd66ca4ebac603f6fb6faedb83b5c7"
|
|
12196
12196
|
},
|
|
12197
12197
|
{
|
|
12198
12198
|
"path": "tests/publish_gate.mjs",
|
|
@@ -12987,7 +12987,7 @@
|
|
|
12987
12987
|
{
|
|
12988
12988
|
"path": "tests/upgrade.mjs",
|
|
12989
12989
|
"mode": "0000644",
|
|
12990
|
-
"sha256": "
|
|
12990
|
+
"sha256": "492b9dc7efdd6f63b81f1662d5fdc9a7541b7f8ccc6c0ec77a456ad3f574ee74"
|
|
12991
12991
|
},
|
|
12992
12992
|
{
|
|
12993
12993
|
"path": "tests/upgrade_persist_pin.mjs",
|
package/.gitattributes
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*.bash text eol=lf
|
|
19
19
|
.husky/pre-commit text eol=lf
|
|
20
20
|
.husky/pre-push text eol=lf
|
|
21
|
+
.husky/post-merge text eol=lf
|
|
21
22
|
|
|
22
23
|
# Node / Python sources: keep LF everywhere too. Node tolerates CRLF, but LF
|
|
23
24
|
# keeps diffs clean and avoids surprises when these are sourced or piped.
|
|
@@ -57,8 +58,13 @@
|
|
|
57
58
|
# publish-reconciler runs a temp-clone merge with this driver registered, so a PR
|
|
58
59
|
# that conflicts only on these files lands without a human (GitHub's own auto-merge
|
|
59
60
|
# can't run a custom driver, hence the server does the merge locally then pushes).
|
|
60
|
-
# NB: the TOP-LEVEL CLAUDE.md
|
|
61
|
-
#
|
|
61
|
+
# NB (task 1001431): the TOP-LEVEL CLAUDE.md IS listed now. It used to be excluded as
|
|
62
|
+
# "hand-written", and that was true until ADR 0062 §8 gave its §13 a generated
|
|
63
|
+
# BEGIN/END SESSION-LOG SNIPPET block — after which two sessions each adding a session-log
|
|
64
|
+
# file conflicted there on every merge with nothing to heal it. It is the same shape as
|
|
65
|
+
# docs/file-map.md (hand-written prose + a generated block), not the whole-file shape, so
|
|
66
|
+
# the driver 3-way-merges the prose and re-renders only the snippet; a genuine conflict in
|
|
67
|
+
# the prose still falls through to a normal git conflict. scripts/gds/CLAUDE.md
|
|
62
68
|
# LEFT that set in task 1003548 (its ~40k-char block was 76% of a file loaded on every
|
|
63
69
|
# visit to that directory; docs/repo-map.md still carries the full index), so its entry
|
|
64
70
|
# came out with it — a path listed here whose generator no longer owns it would route a
|
|
@@ -72,6 +78,14 @@
|
|
|
72
78
|
# appear in a merge and these two driver entries never fire. They are KEPT — harmless —
|
|
73
79
|
# as a transition backstop for any in-flight branch that still carries a committed copy;
|
|
74
80
|
# once none remain they can be dropped. The other four entries stay load-bearing.
|
|
81
|
+
# The LEADING SLASH is load-bearing: a gitattributes pattern with no slash matches at ANY
|
|
82
|
+
# depth, so a bare `CLAUDE.md` would also capture all 22 modules/*/CLAUDE.md and
|
|
83
|
+
# scripts/gds/CLAUDE.md — files the driver does NOT know (they are not in NESTED_DIRS, so
|
|
84
|
+
# REGENERABLE has no rule for them). The driver would then exit 1 on each and FORCE a
|
|
85
|
+
# conflict git would otherwise have merged cleanly — the exact 'routed through a driver
|
|
86
|
+
# that cannot regenerate it' failure this file warns about above. `/CLAUDE.md` anchors to
|
|
87
|
+
# the repo root. Verified with `git check-attr merge -- modules/economy/CLAUDE.md`.
|
|
88
|
+
/CLAUDE.md merge=otb-regen
|
|
75
89
|
docs/repo-map.md merge=otb-regen
|
|
76
90
|
docs/file-map.md merge=otb-regen
|
|
77
91
|
docs/session-log-index.md merge=otb-regen
|
|
@@ -98,6 +112,34 @@ src/bongos/routes/CLAUDE.md merge=otb-regen
|
|
|
98
112
|
docs/copy-registry.json merge=otb-regen
|
|
99
113
|
docs/copy-inventory.md merge=otb-regen text eol=lf
|
|
100
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
|
+
|
|
101
143
|
# Append-only index tables (task 1385 / ADR 0082). docs/adr/README.md is a
|
|
102
144
|
# hand-maintained ADR index whose ONLY recurring conflict is two branches each
|
|
103
145
|
# appending a new row — there is no generator to regen it from, so `otb-regen`
|
|
@@ -384,7 +384,6 @@ export interface PostTasksIdMergeRequest { into_task_id: number; reason?: string
|
|
|
384
384
|
export interface PostTasksIdOverrideRequestRequest { rationale: string }
|
|
385
385
|
export interface PostTasksIdOverrideRequestResponse { ok: boolean; request: unknown }
|
|
386
386
|
export interface PostTasksIdPublishBranchRequest { branch: string; head_sha: string; bundle_b64: string; value_summary?: string }
|
|
387
|
-
export interface PostTasksIdPublishBranchResponse { ok: boolean; pr_url: unknown; pr_number: unknown; head_sha: unknown; auto_merge: unknown }
|
|
388
387
|
export interface PostTasksIdShipResponse { ok: boolean; task: unknown }
|
|
389
388
|
export interface PostTasksIdVisualRequest { image_b64: string; content_type: string; alt?: string }
|
|
390
389
|
export interface PostTasksIdVisualResponse { ok: boolean; visual_url: unknown; visual_alt: unknown; bytes: unknown }
|
|
@@ -1203,7 +1202,7 @@ export interface BongosClient {
|
|
|
1203
1202
|
/** POST /tasks/{id}/promote — rank: metic+archon */
|
|
1204
1203
|
postTasksIdPromote(args?: RequestArgs): Promise<ApiResponse>;
|
|
1205
1204
|
/** POST /tasks/{id}/publish-branch — rank: any-builder */
|
|
1206
|
-
postTasksIdPublishBranch(args: RequestArgs & { body: PostTasksIdPublishBranchRequest }): Promise<
|
|
1205
|
+
postTasksIdPublishBranch(args: RequestArgs & { body: PostTasksIdPublishBranchRequest }): Promise<ApiResponse>;
|
|
1207
1206
|
/** GET /tasks/{id}/publish-status — rank: any-builder */
|
|
1208
1207
|
getTasksIdPublishStatus(args?: RequestArgs): Promise<ApiResponse>;
|
|
1209
1208
|
/** GET /tasks/{id}/recommendations — rank: any-builder */
|
package/docs/api/openapi.json
CHANGED
|
@@ -15704,14 +15704,7 @@
|
|
|
15704
15704
|
},
|
|
15705
15705
|
"responses": {
|
|
15706
15706
|
"200": {
|
|
15707
|
-
"description": "Success."
|
|
15708
|
-
"content": {
|
|
15709
|
-
"application/json": {
|
|
15710
|
-
"schema": {
|
|
15711
|
-
"$ref": "#/components/schemas/PostTasksIdPublishBranchResponse"
|
|
15712
|
-
}
|
|
15713
|
-
}
|
|
15714
|
-
}
|
|
15707
|
+
"description": "Success."
|
|
15715
15708
|
},
|
|
15716
15709
|
"400": {
|
|
15717
15710
|
"$ref": "#/components/responses/ValidationFailed"
|
|
@@ -22442,25 +22435,6 @@
|
|
|
22442
22435
|
],
|
|
22443
22436
|
"additionalProperties": false
|
|
22444
22437
|
},
|
|
22445
|
-
"PostTasksIdPublishBranchResponse": {
|
|
22446
|
-
"type": "object",
|
|
22447
|
-
"properties": {
|
|
22448
|
-
"ok": {
|
|
22449
|
-
"type": "boolean"
|
|
22450
|
-
},
|
|
22451
|
-
"pr_url": {},
|
|
22452
|
-
"pr_number": {},
|
|
22453
|
-
"head_sha": {},
|
|
22454
|
-
"auto_merge": {}
|
|
22455
|
-
},
|
|
22456
|
-
"required": [
|
|
22457
|
-
"ok",
|
|
22458
|
-
"pr_url",
|
|
22459
|
-
"pr_number",
|
|
22460
|
-
"head_sha",
|
|
22461
|
-
"auto_merge"
|
|
22462
|
-
]
|
|
22463
|
-
},
|
|
22464
22438
|
"PostTasksIdShipResponse": {
|
|
22465
22439
|
"type": "object",
|
|
22466
22440
|
"properties": {
|
|
@@ -23019,8 +22993,8 @@
|
|
|
23019
22993
|
}
|
|
23020
22994
|
},
|
|
23021
22995
|
"x-endpoint-count": 363,
|
|
23022
|
-
"x-schema-count":
|
|
22996
|
+
"x-schema-count": 401,
|
|
23023
22997
|
"x-undocumented-bodies": 9,
|
|
23024
|
-
"x-response-schemas":
|
|
22998
|
+
"x-response-schemas": 270,
|
|
23025
22999
|
"x-generated-by": "scripts/gds/gen-api-docs.js"
|
|
23026
23000
|
}
|
|
@@ -1819,5 +1819,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1819
1819
|
landed since 1.19.678 with no explicit bump. run 34657605820. (task 1002620)
|
|
1820
1820
|
1.19.680 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1821
1821
|
landed since 1.19.679 with no explicit bump. run 34665216511. (task 1002620)
|
|
1822
|
+
1.19.681 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
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)
|
|
1822
1826
|
---------------------------------------------------------------------------
|
|
1823
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.
|
|
@@ -513,8 +513,21 @@ async function openOrFindPullRequest({ branch, title, body }, deps = {}) {
|
|
|
513
513
|
// without auto-merge enabled, or a PR already mergeable, returns false and the
|
|
514
514
|
// caller surfaces the open PR (exactly like ciLand on a failed enable).
|
|
515
515
|
async function enableAutoMerge({ nodeId }, deps = {}) {
|
|
516
|
+
return (await enableAutoMergeDetailed({ nodeId }, deps)).ok;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// enableAutoMergeDetailed — the same arming call, but it says WHY it failed
|
|
520
|
+
// (task 1001431). enableAutoMerge collapses every failure to `false`, so the route
|
|
521
|
+
// answered a builder with `auto_merge: false` and no cause, and the single most
|
|
522
|
+
// common cause is not an outage at all: a repository with `allow_auto_merge` OFF
|
|
523
|
+
// can never be armed, so every publish reports the same silent false forever and
|
|
524
|
+
// the PR waits for a human nobody told. Returns { ok, reason } — reason is null on
|
|
525
|
+
// success, else a short scrubbed string safe to hand the (ownership-gated) builder.
|
|
526
|
+
// enableAutoMerge stays the boolean façade so existing callers are untouched.
|
|
527
|
+
async function enableAutoMergeDetailed({ nodeId }, deps = {}) {
|
|
516
528
|
const token = await resolveToken(deps);
|
|
517
|
-
if (!token
|
|
529
|
+
if (!token) return { ok: false, reason: 'no GitHub token could be resolved' };
|
|
530
|
+
if (!nodeId) return { ok: false, reason: 'the pull request has no node id to arm' };
|
|
518
531
|
const doFetch = deps.fetchImpl || fetch;
|
|
519
532
|
const query = `mutation($id:ID!){enablePullRequestAutoMerge(input:{pullRequestId:$id,mergeMethod:MERGE}){clientMutationId}}`;
|
|
520
533
|
try {
|
|
@@ -523,11 +536,15 @@ async function enableAutoMerge({ nodeId }, deps = {}) {
|
|
|
523
536
|
headers: { ...ghHeaders(token), 'Content-Type': 'application/json' },
|
|
524
537
|
body: JSON.stringify({ query, variables: { id: nodeId } }),
|
|
525
538
|
});
|
|
526
|
-
if (!res.ok) return false;
|
|
539
|
+
if (!res.ok) return { ok: false, reason: `GitHub GraphQL returned HTTP ${res.status}` };
|
|
527
540
|
const out = await res.json();
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
541
|
+
if (!out || !out.errors) return { ok: true, reason: null };
|
|
542
|
+
// GitHub answers a repo with auto-merge disabled with a plain GraphQL error;
|
|
543
|
+
// its own message is the actionable one, so pass it through (truncated).
|
|
544
|
+
const msg = out.errors.map((e) => e && e.message).filter(Boolean).join('; ');
|
|
545
|
+
return { ok: false, reason: scrubCredential(msg || 'GitHub GraphQL rejected the mutation').slice(0, 300) };
|
|
546
|
+
} catch (e) {
|
|
547
|
+
return { ok: false, reason: scrubCredential((e && e.message) || 'the GraphQL request threw').slice(0, 300) };
|
|
531
548
|
}
|
|
532
549
|
}
|
|
533
550
|
|
|
@@ -1388,6 +1405,7 @@ module.exports = {
|
|
|
1388
1405
|
pushBundle,
|
|
1389
1406
|
openOrFindPullRequest,
|
|
1390
1407
|
enableAutoMerge,
|
|
1408
|
+
enableAutoMergeDetailed,
|
|
1391
1409
|
getPublishStatus,
|
|
1392
1410
|
getMergeState,
|
|
1393
1411
|
mergeIfGreen,
|
|
@@ -67,6 +67,33 @@ const { MIGRATION_KEYWORD_RE, VALID_FITS, badKind, deHashRefs, detectsMigration,
|
|
|
67
67
|
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
68
68
|
const log = api.logger('lifecycle');
|
|
69
69
|
const registerTaskWriteRoutes = require('./task-write-routes.js'); // task 1003206 — POST /tasks + PATCH /tasks/:id
|
|
70
|
+
// The body of the generic `publish_failed` 500 from POST /tasks/:id/publish-branch.
|
|
71
|
+
// Pure and exported so the response a builder actually receives is testable without
|
|
72
|
+
// standing up the route (the file's existing convention — see rankAttestsGateTrust).
|
|
73
|
+
//
|
|
74
|
+
// task 1001431 added `code` + `detail`. Seven error codes get their own mapped response
|
|
75
|
+
// above the fallthrough; every OTHER code github-push can throw — APP_TOKEN_FAILED,
|
|
76
|
+
// GRAPHQL_FAILED, BAD_PR, PR_PATCH_FAILED, STATUS_FETCH_FAILED — arrived here and was
|
|
77
|
+
// dropped on the floor, so the builder saw a bare `publish_failed` with nothing to act
|
|
78
|
+
// on and no way to tell an expired App token from a GraphQL outage. The code is an
|
|
79
|
+
// internal constant rather than user data, and this route is ownership-gated (claim
|
|
80
|
+
// holder or Archon) and already returns scrubbed git stderr — a short code name is
|
|
81
|
+
// strictly less revealing than what it ships beside.
|
|
82
|
+
//
|
|
83
|
+
// Every field stays OPTIONAL and keeps its existing name and meaning: ship.js prints
|
|
84
|
+
// `reason`, and older readers must not start seeing a key change shape under them.
|
|
85
|
+
function publishFailedDetail(e = {}) {
|
|
86
|
+
return {
|
|
87
|
+
...(e.code ? { code: e.code } : {}),
|
|
88
|
+
...(e.detail ? { detail: e.detail } : {}),
|
|
89
|
+
...(e.git_step ? { step: e.git_step } : {}),
|
|
90
|
+
...(e.stderr ? { reason: e.stderr } : {}),
|
|
91
|
+
...(e.stderr_bytes != null ? { reason_bytes: e.stderr_bytes } : {}),
|
|
92
|
+
...(e.exit_code != null ? { git_exit_code: e.exit_code } : {}),
|
|
93
|
+
...(e.timed_out ? { timed_out: true } : {}),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
70
97
|
module.exports = function buildTasksRouter() {
|
|
71
98
|
const router = express.Router();
|
|
72
99
|
|
|
@@ -1076,7 +1103,11 @@ module.exports = function buildTasksRouter() {
|
|
|
1076
1103
|
`Merging triggers .github/workflows/deploy-prod.yml -> ${branding().domains.publicOrigin}. ` +
|
|
1077
1104
|
`Auto-merge is enabled — GitHub lands it once the required checks pass.`;
|
|
1078
1105
|
const pr = await githubPush.openOrFindPullRequest({ branch, title, body });
|
|
1079
|
-
|
|
1106
|
+
// task 1001431: ask WHY when arming fails. `auto_merge: false` on its own told the
|
|
1107
|
+
// builder nothing, and the commonest cause is a repo with allow_auto_merge OFF —
|
|
1108
|
+
// a permanent condition that reads exactly like a transient blip.
|
|
1109
|
+
const autoMergeResult = await githubPush.enableAutoMergeDetailed({ nodeId: pr.node_id });
|
|
1110
|
+
const autoMerge = autoMergeResult.ok;
|
|
1080
1111
|
// task 1139 (ADR 0058): attest the author's trust tier on the pushed sha so the
|
|
1081
1112
|
// gate-review check can auto-approve a Metic+ author's gate-surface PR with no
|
|
1082
1113
|
// owner click. Only the SERVER can set this (it holds req.builder's server-
|
|
@@ -1098,6 +1129,8 @@ module.exports = function buildTasksRouter() {
|
|
|
1098
1129
|
}
|
|
1099
1130
|
return res.status(201).json({
|
|
1100
1131
|
ok: true, pr_url: pr.url, pr_number: pr.number, head_sha: pushedSha, auto_merge: autoMerge,
|
|
1132
|
+
// Only present when arming failed — the publish itself still succeeded (201).
|
|
1133
|
+
...(autoMerge ? {} : { auto_merge_reason: autoMergeResult.reason }),
|
|
1101
1134
|
});
|
|
1102
1135
|
} catch (e) {
|
|
1103
1136
|
if (e.code === 'TIP_MISMATCH') {
|
|
@@ -1134,13 +1167,7 @@ module.exports = function buildTasksRouter() {
|
|
|
1134
1167
|
// `reason` keeps its name and position so existing readers (ship.js prints it)
|
|
1135
1168
|
// keep working; it is simply no longer amputated. Everything here is already
|
|
1136
1169
|
// credential-scrubbed at the throw site by github-push.scrubCredential.
|
|
1137
|
-
return res.fail('publish_failed', 500,
|
|
1138
|
-
...(e.git_step ? { step: e.git_step } : {}),
|
|
1139
|
-
...(e.stderr ? { reason: e.stderr } : {}),
|
|
1140
|
-
...(e.stderr_bytes != null ? { reason_bytes: e.stderr_bytes } : {}),
|
|
1141
|
-
...(e.exit_code != null ? { git_exit_code: e.exit_code } : {}),
|
|
1142
|
-
...(e.timed_out ? { timed_out: true } : {}),
|
|
1143
|
-
});
|
|
1170
|
+
return res.fail('publish_failed', 500, publishFailedDetail(e));
|
|
1144
1171
|
} finally {
|
|
1145
1172
|
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
|
|
1146
1173
|
}
|
|
@@ -1336,6 +1363,7 @@ module.exports.MIGRATION_KEYWORD_RE = MIGRATION_KEYWORD_RE;
|
|
|
1336
1363
|
// #871: exported so tests/task_ownership_gate.mjs can drive the claim-ownership
|
|
1337
1364
|
// gate directly (with a stubbed db.getLastClaimHolder) — no server/DB needed.
|
|
1338
1365
|
module.exports.gateTaskOwnership = gateTaskOwnership;
|
|
1366
|
+
module.exports.publishFailedDetail = publishFailedDetail;
|
|
1339
1367
|
// task 1053 (F): exported so the verify-attestation guard is unit-tested.
|
|
1340
1368
|
module.exports.isVerifyTask = isVerifyTask;
|
|
1341
1369
|
// task 1139 / 1169 (ADR 0058): exported so the pure rank-attestation decision
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
491
|
-
|
|
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 {
|
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
// mergeMainIntoBranch self-heal so the heal runs even where the driver isn't
|
|
17
17
|
// installed per-clone.
|
|
18
18
|
//
|
|
19
|
+
// THE DRIVER ONLY FIRES ON A CONFLICT. A merge that lands CLEAN still leaves the
|
|
20
|
+
// generated docs describing the pre-merge tree — new symbols, a new session log and a
|
|
21
|
+
// new skill all arrive without conflicting. Three things close that: ship.js (at ship
|
|
22
|
+
// time), the server's conflict-resolve.js step 7b (for a PR it resolves), and
|
|
23
|
+
// `.husky/post-merge` (task 1001431) for the bare local `git merge` / `git pull` that
|
|
24
|
+
// neither of those covers.
|
|
25
|
+
//
|
|
19
26
|
// SAFETY: it regenerates the GENERATED content and 3-way-merges the hand-written
|
|
20
27
|
// parts of nested docs; it succeeds ONLY if no conflict markers remain. A genuine
|
|
21
28
|
// hand-written conflict falls through to a normal git conflict — it never invents a
|
|
@@ -47,9 +54,40 @@ const G = require('./gen-repo-map');
|
|
|
47
54
|
const FM = require('./gen-file-map');
|
|
48
55
|
const SI = require('./gen-session-index');
|
|
49
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.
|
|
50
63
|
|
|
51
64
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
52
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
|
+
|
|
53
91
|
// The files this driver knows how to regenerate. docs/repo-map.md and
|
|
54
92
|
// docs/session-log-index.md are whole-file; each NESTED_DIRS CLAUDE.md is
|
|
55
93
|
// hand-written prose with one generated block; docs/file-map.md is hand-written
|
|
@@ -61,13 +99,25 @@ const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
|
61
99
|
// branches that both touch hall/status/landing copy regenerate them differently
|
|
62
100
|
// and conflict on ordinary text; they join the whole-file group for the same
|
|
63
101
|
// reason session-log-index.md did.
|
|
102
|
+
// task 1001431: the ROOT CLAUDE.md joined too. It is mostly hand-written, but §13 carries
|
|
103
|
+
// the bounded BEGIN/END GENERATED SESSION-LOG SNIPPET block (ADR 0062 §8) — so two sessions
|
|
104
|
+
// that each add a session-log file regenerate that block differently and ALWAYS conflict
|
|
105
|
+
// there, and nothing healed it: the driver did not know the file, and `merge=union` would be
|
|
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.
|
|
64
111
|
const REGENERABLE = new Set([
|
|
112
|
+
'CLAUDE.md',
|
|
65
113
|
'docs/repo-map.md',
|
|
66
114
|
'docs/file-map.md',
|
|
67
115
|
'docs/session-log-index.md',
|
|
68
116
|
'docs/copy-inventory.md',
|
|
69
117
|
'docs/copy-registry.json',
|
|
70
118
|
...G.NESTED_DIRS.map((d) => `${d}/CLAUDE.md`),
|
|
119
|
+
...API_DOC_FILES,
|
|
120
|
+
...API_CLIENT_FILES,
|
|
71
121
|
]);
|
|
72
122
|
|
|
73
123
|
// A line that begins with a git conflict marker. If any survives our resolution,
|
|
@@ -124,6 +174,49 @@ function regenerateResolved(relpath, { base = '', ours = '', theirs = '' } = {})
|
|
|
124
174
|
return { content: CI.renderReport(CI.buildRegistry()), ok: true };
|
|
125
175
|
}
|
|
126
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
|
+
|
|
204
|
+
// The ROOT CLAUDE.md (task 1001431): hand-written prose carrying ONE generated block, the
|
|
205
|
+
// §13 session-log snippet. 3-way-merge the prose, then re-render the snippet from the
|
|
206
|
+
// session-log files on disk. Must be handled BEFORE the nested-CLAUDE.md fallthrough below,
|
|
207
|
+
// which derives a directory by stripping `/CLAUDE.md` — for the root path that strips
|
|
208
|
+
// nothing and would ask gen-repo-map for a symbol block for a directory named "CLAUDE.md".
|
|
209
|
+
// It also needs none of the code-symbol model, so it sits with the other pre-buildModel rules.
|
|
210
|
+
// Same provisionality caveat as the whole-file rules above: collectEntries() scans the
|
|
211
|
+
// working tree, which during a merge is still ours, so the block this writes describes our
|
|
212
|
+
// side. That is what makes the merge COMPLETE; the post-merge regeneration (conflict-resolve
|
|
213
|
+
// step 7b / ship-preflight-steps' resolveInWorktree) is what makes it CORRECT.
|
|
214
|
+
if (relpath === 'CLAUDE.md') {
|
|
215
|
+
const merged = threeWayMerge(base, ours, theirs);
|
|
216
|
+
const out = SI.injectSnippet(merged, SI.renderSnippet(SI.collectEntries()));
|
|
217
|
+
return { content: out, ok: !hasMarkers(out) };
|
|
218
|
+
}
|
|
219
|
+
|
|
127
220
|
const model = G.buildModel();
|
|
128
221
|
|
|
129
222
|
// docs/repo-map.md is fully generated — ignore all three sides, rebuild it.
|
package/scripts/gds/init.js
CHANGED
|
@@ -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 =
|
|
946
|
+
const vendorRel = upgrade.vendorRelFor(version);
|
|
947
947
|
coreDep = `file:${vendorRel}`;
|
|
948
948
|
vendorPlan = { tgz, version, vendorRel };
|
|
949
949
|
}
|
package/scripts/gds/upgrade.js
CHANGED
|
@@ -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 :
|
|
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 =
|
|
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) {
|
package/scripts/gds/worktree.js
CHANGED
|
@@ -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.
|
|
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');
|
|
@@ -554,9 +554,27 @@ test('1003546 every REGENERABLE artifact has a producer, and every producer is i
|
|
|
554
554
|
['docs/repo-map.md', 'scripts/gds/gen-repo-map.js'],
|
|
555
555
|
['docs/file-map.md', 'scripts/gds/gen-file-map.js'],
|
|
556
556
|
['docs/session-log-index.md', 'scripts/gds/gen-session-index.js'],
|
|
557
|
+
// task 1001431 — the ROOT CLAUDE.md joined REGENERABLE for its §13 snippet block.
|
|
558
|
+
// gen-session-index.js writes BOTH the (gitignored) whole index and that tracked
|
|
559
|
+
// snippet, and it is already in GENERATOR_SCRIPTS, so the server's post-merge
|
|
560
|
+
// regeneration covers the new entry with no change to the generator list.
|
|
561
|
+
['CLAUDE.md', 'scripts/gds/gen-session-index.js'],
|
|
557
562
|
[CI.REGISTRY_REL, 'scripts/gds/copy-inventory.js'],
|
|
558
563
|
[CI.REPORT_REL, 'scripts/gds/copy-inventory.js'],
|
|
559
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']),
|
|
560
578
|
]);
|
|
561
579
|
|
|
562
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
|
|
|
@@ -45,6 +51,9 @@ const repo = path.resolve(here, '..');
|
|
|
45
51
|
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');
|
|
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
|
|
48
57
|
|
|
49
58
|
const MARKERS = /^(<{7}|={7}|>{7})/m;
|
|
50
59
|
|
|
@@ -120,10 +129,67 @@ function withBlockBody(src, body) {
|
|
|
120
129
|
|
|
121
130
|
// ── 5) a non-generated path is never in scope ───────────────────────────────────
|
|
122
131
|
{
|
|
123
|
-
assert.equal(regen.REGENERABLE.has('CLAUDE.md'), false, 'top-level CLAUDE.md is hand-written — never auto-healed');
|
|
124
132
|
assert.equal(regen.REGENERABLE.has('scripts/gds/box.js'), false, 'source files are never auto-healed');
|
|
125
133
|
const r = regen.regenerateResolved('scripts/gds/box.js', { base: 'a', ours: 'b', theirs: 'c' });
|
|
126
134
|
assert.equal(r.content, null, 'a non-generated path resolves to null content');
|
|
135
|
+
|
|
136
|
+
// REVERSAL (task 1001431). This block used to assert the opposite —
|
|
137
|
+
// REGENERABLE.has('CLAUDE.md') === false, 'top-level CLAUDE.md is hand-written'
|
|
138
|
+
// — and that was correct when it was written. ADR 0062 §8 then gave the root
|
|
139
|
+
// CLAUDE.md a generated §13 BEGIN/END SESSION-LOG SNIPPET block, which made the
|
|
140
|
+
// premise false without anything noticing: two sessions each adding a session-log
|
|
141
|
+
// file regenerate that block differently and conflict there on EVERY merge, and no
|
|
142
|
+
// layer healed it (not the driver, not ship.js, not the server resolver).
|
|
143
|
+
assert.equal(regen.REGENERABLE.has('CLAUDE.md'), true,
|
|
144
|
+
'the root CLAUDE.md carries a generated §13 snippet block and IS healed (task 1001431)');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── 5b) the root CLAUDE.md heals: prose 3-way-merged, snippet re-rendered ────────
|
|
148
|
+
// It is the docs/file-map.md shape (hand-written prose + a generated block), NOT the
|
|
149
|
+
// whole-file shape — so a conflict in the PROSE must still fall through to a human.
|
|
150
|
+
{
|
|
151
|
+
const realRoot = fs.readFileSync(path.join(repo, 'CLAUDE.md'), 'utf8');
|
|
152
|
+
const freshSnippet = SI.renderSnippet(SI.collectEntries());
|
|
153
|
+
|
|
154
|
+
// A conflict confined to the generated block heals, and the block comes out equal to
|
|
155
|
+
// a fresh render regardless of what either side had in it.
|
|
156
|
+
const mangle = (tag) => SI.injectSnippet(realRoot, `${SI.BEGIN}\n- ${tag}\n${SI.END}`);
|
|
157
|
+
const healed = regen.regenerateResolved('CLAUDE.md', {
|
|
158
|
+
base: realRoot, ours: mangle('OURS entry'), theirs: mangle('THEIRS entry'),
|
|
159
|
+
});
|
|
160
|
+
assert.equal(healed.ok, true, 'a conflict inside the generated snippet must heal');
|
|
161
|
+
assert.ok(!MARKERS.test(healed.content), 'no conflict markers survive the heal');
|
|
162
|
+
assert.ok(healed.content.includes(freshSnippet), 'the snippet is re-rendered from the session-log files');
|
|
163
|
+
assert.ok(!healed.content.includes('OURS entry') && !healed.content.includes('THEIRS entry'),
|
|
164
|
+
'neither side\'s stale snippet content survives');
|
|
165
|
+
|
|
166
|
+
// Hand-written PROSE that genuinely diverges is NOT auto-resolved.
|
|
167
|
+
const firstNL = realRoot.indexOf('\n');
|
|
168
|
+
const proseConflict = regen.regenerateResolved('CLAUDE.md', {
|
|
169
|
+
base: realRoot,
|
|
170
|
+
ours: '# OURS heading' + realRoot.slice(firstNL),
|
|
171
|
+
theirs: '# THEIRS heading' + realRoot.slice(firstNL),
|
|
172
|
+
});
|
|
173
|
+
assert.equal(proseConflict.ok, false, 'a real prose conflict must fall through to a human');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── 5c) the .gitattributes pattern for the root CLAUDE.md is ROOT-ANCHORED ───────
|
|
177
|
+
// `/CLAUDE.md` vs `CLAUDE.md` is the whole ballgame: a gitattributes pattern with no
|
|
178
|
+
// slash matches at ANY depth, so the bare spelling would also capture the 22
|
|
179
|
+
// modules/*/CLAUDE.md files and scripts/gds/CLAUDE.md — none of which are in
|
|
180
|
+
// NESTED_DIRS, so the driver has no rule for them, exits 1, and FORCES a conflict git
|
|
181
|
+
// would otherwise merge cleanly. Asserted through git itself, not by reading the file,
|
|
182
|
+
// because the pattern semantics are git's and not ours to restate.
|
|
183
|
+
{
|
|
184
|
+
const attr = (p) => {
|
|
185
|
+
const r = spawnSync('git', ['check-attr', 'merge', '--', p], { cwd: repo, encoding: 'utf8' });
|
|
186
|
+
return (r.stdout || '').trim();
|
|
187
|
+
};
|
|
188
|
+
assert.match(attr('CLAUDE.md'), /otb-regen$/, 'the root CLAUDE.md must get the driver');
|
|
189
|
+
for (const p of ['modules/economy/CLAUDE.md', 'scripts/gds/CLAUDE.md']) {
|
|
190
|
+
assert.doesNotMatch(attr(p), /otb-regen/,
|
|
191
|
+
`${p} is not regenerable — the root pattern must be anchored so it is not captured`);
|
|
192
|
+
}
|
|
127
193
|
}
|
|
128
194
|
|
|
129
195
|
// ── 6) END-TO-END: a real `git merge` invokes the driver and auto-resolves ──────
|
|
@@ -193,7 +259,11 @@ function withBlockBody(src, body) {
|
|
|
193
259
|
.filter((line) => line && !line.startsWith('#'))
|
|
194
260
|
.map((line) => line.split(/\s+/))
|
|
195
261
|
.filter((tokens) => tokens.slice(1).includes('merge=otb-regen'))
|
|
196
|
-
|
|
262
|
+
// Strip a leading `/`: in a gitattributes pattern that is a ROOT ANCHOR, not part of
|
|
263
|
+
// the path (task 1001431 — `/CLAUDE.md` must not also match modules/*/CLAUDE.md). The
|
|
264
|
+
// driver receives git's %P, which never carries it, so REGENERABLE keys are unanchored
|
|
265
|
+
// and the two lists are compared as the PATH sets they are.
|
|
266
|
+
.map((tokens) => tokens[0].replace(/^\//, ''));
|
|
197
267
|
assert.ok(otbRegenPaths.length > 0, 'expected at least one merge=otb-regen line in .gitattributes');
|
|
198
268
|
for (const p of otbRegenPaths) {
|
|
199
269
|
assert.ok(regen.REGENERABLE.has(p), `.gitattributes declares merge=otb-regen for ${p} but REGENERABLE (git-merge-regen.js) does not know it`);
|
|
@@ -376,4 +446,56 @@ function withBlockBody(src, body) {
|
|
|
376
446
|
}
|
|
377
447
|
}
|
|
378
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
|
+
|
|
379
501
|
console.log('git_merge_regen: all assertions passed');
|
|
@@ -194,6 +194,63 @@ await t('enableAutoMerge: posts the graphql mutation; false on a graphql error',
|
|
|
194
194
|
assert.equal(await gp.enableAutoMerge({ nodeId: null }, { token: 'TT', fetchImpl: okFetch }), false, 'no node id → false');
|
|
195
195
|
});
|
|
196
196
|
|
|
197
|
+
// ---- publishFailedDetail: the generic 500 names its cause (task 1001431) --
|
|
198
|
+
await t('publishFailedDetail: threads the error code so publish_failed is never bare', async () => {
|
|
199
|
+
const D = buildTasksRouter.publishFailedDetail;
|
|
200
|
+
|
|
201
|
+
// The regression this closes: a code with no git_step/stderr used to produce {}, so the
|
|
202
|
+
// builder received `publish_failed` and nothing else — an expired App token and a
|
|
203
|
+
// GraphQL outage were indistinguishable.
|
|
204
|
+
assert.deepEqual(D({ code: 'APP_TOKEN_FAILED' }), { code: 'APP_TOKEN_FAILED' });
|
|
205
|
+
assert.deepEqual(D({ code: 'GRAPHQL_FAILED', detail: '[{"type":"FORBIDDEN"}]' }),
|
|
206
|
+
{ code: 'GRAPHQL_FAILED', detail: '[{"type":"FORBIDDEN"}]' });
|
|
207
|
+
|
|
208
|
+
// Existing field names + meanings are unchanged — ship.js prints `reason`.
|
|
209
|
+
assert.deepEqual(
|
|
210
|
+
D({ code: 'GIT_FAILED', git_step: 'push', stderr: 'Permission denied', stderr_bytes: 17, exit_code: 128, timed_out: true }),
|
|
211
|
+
{ code: 'GIT_FAILED', step: 'push', reason: 'Permission denied', reason_bytes: 17, git_exit_code: 128, timed_out: true }
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// Absent fields stay absent rather than serializing as null/undefined keys.
|
|
215
|
+
assert.deepEqual(D({}), {});
|
|
216
|
+
assert.deepEqual(D(), {});
|
|
217
|
+
assert.equal('timed_out' in D({ timed_out: false }), false, 'a falsy timed_out is omitted, not sent as false');
|
|
218
|
+
assert.equal('git_exit_code' in D({ exit_code: 0 }), true, 'exit_code 0 is real and must survive');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ---- enableAutoMergeDetailed: says WHY it could not arm (task 1001431) ----
|
|
222
|
+
await t('enableAutoMergeDetailed: reports the GitHub message instead of a bare false', async () => {
|
|
223
|
+
const okFetch = async () => jsonRes(200, { data: { enablePullRequestAutoMerge: {} } });
|
|
224
|
+
assert.deepEqual(await gp.enableAutoMergeDetailed({ nodeId: 'N1' }, { token: 'TT', fetchImpl: okFetch }),
|
|
225
|
+
{ ok: true, reason: null });
|
|
226
|
+
|
|
227
|
+
// The real-world case: a repository with allow_auto_merge OFF. GitHub answers with a
|
|
228
|
+
// plain GraphQL error, and its own message is the actionable one.
|
|
229
|
+
const offFetch = async () => jsonRes(200, { errors: [{ message: 'Auto-merge is not allowed for this repository' }] });
|
|
230
|
+
const off = await gp.enableAutoMergeDetailed({ nodeId: 'N1' }, { token: 'TT', fetchImpl: offFetch });
|
|
231
|
+
assert.equal(off.ok, false);
|
|
232
|
+
assert.match(off.reason, /Auto-merge is not allowed/, 'the repo-level cause reaches the caller');
|
|
233
|
+
|
|
234
|
+
const httpFetch = async () => jsonRes(502, {});
|
|
235
|
+
const http = await gp.enableAutoMergeDetailed({ nodeId: 'N1' }, { token: 'TT', fetchImpl: httpFetch });
|
|
236
|
+
assert.equal(http.ok, false);
|
|
237
|
+
assert.match(http.reason, /HTTP 502/, 'an HTTP failure is distinguishable from a rejection');
|
|
238
|
+
|
|
239
|
+
const thrown = await gp.enableAutoMergeDetailed({ nodeId: 'N1' }, {
|
|
240
|
+
token: 'TT', fetchImpl: async () => { throw new Error('socket hang up'); },
|
|
241
|
+
});
|
|
242
|
+
assert.equal(thrown.ok, false);
|
|
243
|
+
assert.match(thrown.reason, /socket hang up/);
|
|
244
|
+
|
|
245
|
+
assert.equal((await gp.enableAutoMergeDetailed({ nodeId: null }, { token: 'TT', fetchImpl: okFetch })).ok, false);
|
|
246
|
+
assert.match((await gp.enableAutoMergeDetailed({ nodeId: null }, { token: 'TT', fetchImpl: okFetch })).reason,
|
|
247
|
+
/no node id/, 'a missing node id is named, not silently false');
|
|
248
|
+
|
|
249
|
+
// The boolean façade keeps its exact contract for every existing caller.
|
|
250
|
+
assert.equal(await gp.enableAutoMerge({ nodeId: 'N1' }, { token: 'TT', fetchImpl: okFetch }), true);
|
|
251
|
+
assert.equal(await gp.enableAutoMerge({ nodeId: 'N1' }, { token: 'TT', fetchImpl: offFetch }), false);
|
|
252
|
+
});
|
|
253
|
+
|
|
197
254
|
// ---- getPublishStatus: the three states -----------------------------------
|
|
198
255
|
await t('getPublishStatus: open PR → pending; merged + deploy success → success', async () => {
|
|
199
256
|
const mergeSha = 'c'.repeat(40);
|
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
|
-
|
|
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 = [];
|