@bongos/core 1.19.986 → 1.19.987
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 +21 -16
- package/docs/module-api-changelog.md +2 -0
- package/modules/lifecycle/cascade.js +32 -2
- package/modules/lifecycle/routes/lifecycle.js +55 -43
- package/modules/lifecycle/ship-files.js +57 -0
- package/modules/lifecycle/tests/cascade.mjs +147 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/ship-grade.js +2 -2
- package/src/module-api.js +1 -1
- package/tests/grader_reader_lens.mjs +7 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.987",
|
|
6
|
+
"core_contract": "1.19.987",
|
|
7
|
+
"source_commit": "102ad9a7c1c780717451cc48241d577442dc44f1",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-23T19:04:45.567Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 518,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2391,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2935,
|
|
20
|
+
"tree_sha256": "e0b9f64be067a45f8a343766f231127d7bb3708dc50656f188e122f06b15ac2a",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2712,7 +2712,7 @@
|
|
|
2712
2712
|
{
|
|
2713
2713
|
"path": "docs/module-api-changelog.md",
|
|
2714
2714
|
"mode": "0000644",
|
|
2715
|
-
"sha256": "
|
|
2715
|
+
"sha256": "fcad58b07e48eb75f7dbfb280b1d2f167d07a753388aa063d292c7aee323accf"
|
|
2716
2716
|
},
|
|
2717
2717
|
{
|
|
2718
2718
|
"path": "docs/modules-contract.md",
|
|
@@ -5707,7 +5707,7 @@
|
|
|
5707
5707
|
{
|
|
5708
5708
|
"path": "modules/lifecycle/cascade.js",
|
|
5709
5709
|
"mode": "0000644",
|
|
5710
|
-
"sha256": "
|
|
5710
|
+
"sha256": "714bbacc99433c307b949c7f882a081e3dfb77b86c8e472c82a7729b513f7d74"
|
|
5711
5711
|
},
|
|
5712
5712
|
{
|
|
5713
5713
|
"path": "modules/lifecycle/check-rollup.js",
|
|
@@ -6092,7 +6092,7 @@
|
|
|
6092
6092
|
{
|
|
6093
6093
|
"path": "modules/lifecycle/routes/lifecycle.js",
|
|
6094
6094
|
"mode": "0000644",
|
|
6095
|
-
"sha256": "
|
|
6095
|
+
"sha256": "9f311cf078774875e53c4a07ad65501b09d37a3dbfdbe52d38d394ed555100d1"
|
|
6096
6096
|
},
|
|
6097
6097
|
{
|
|
6098
6098
|
"path": "modules/lifecycle/routes/mingle.js",
|
|
@@ -6159,6 +6159,11 @@
|
|
|
6159
6159
|
"mode": "0000644",
|
|
6160
6160
|
"sha256": "bc95dfe95261ddcfdda7f578ba2f4e0e9c1f20d7da955ac5d64800476538bed5"
|
|
6161
6161
|
},
|
|
6162
|
+
{
|
|
6163
|
+
"path": "modules/lifecycle/ship-files.js",
|
|
6164
|
+
"mode": "0000644",
|
|
6165
|
+
"sha256": "7e0797b26e870d33f054708f2ba717589231c8ccada444ebb03c6234452c8d90"
|
|
6166
|
+
},
|
|
6162
6167
|
{
|
|
6163
6168
|
"path": "modules/lifecycle/ship-preflight.js",
|
|
6164
6169
|
"mode": "0000644",
|
|
@@ -6197,7 +6202,7 @@
|
|
|
6197
6202
|
{
|
|
6198
6203
|
"path": "modules/lifecycle/tests/cascade.mjs",
|
|
6199
6204
|
"mode": "0000644",
|
|
6200
|
-
"sha256": "
|
|
6205
|
+
"sha256": "cd0360521e8f89e86fabdf75f41dc213fd2ae5a0ae5a51548441164a915df292"
|
|
6201
6206
|
},
|
|
6202
6207
|
{
|
|
6203
6208
|
"path": "modules/lifecycle/version-autoclose.js",
|
|
@@ -8157,12 +8162,12 @@
|
|
|
8157
8162
|
{
|
|
8158
8163
|
"path": "package-lock.json",
|
|
8159
8164
|
"mode": "0000644",
|
|
8160
|
-
"sha256": "
|
|
8165
|
+
"sha256": "80f51079ed98cab29d9b48f5f20049f58104b76d68645ac01d03a7bdcde9f29e"
|
|
8161
8166
|
},
|
|
8162
8167
|
{
|
|
8163
8168
|
"path": "package.json",
|
|
8164
8169
|
"mode": "0000644",
|
|
8165
|
-
"sha256": "
|
|
8170
|
+
"sha256": "fe62f7efb8e83d82f66dd2277b8ae4da8d448a9286727ea88460ec358f339fc5"
|
|
8166
8171
|
},
|
|
8167
8172
|
{
|
|
8168
8173
|
"path": "public-docs/index.html",
|
|
@@ -9532,7 +9537,7 @@
|
|
|
9532
9537
|
{
|
|
9533
9538
|
"path": "scripts/gds/ship-grade.js",
|
|
9534
9539
|
"mode": "0000644",
|
|
9535
|
-
"sha256": "
|
|
9540
|
+
"sha256": "eb0740f3cdc3dc91038320dd9044dc81c05e31212059d772d5c8bf6e9f44b474"
|
|
9536
9541
|
},
|
|
9537
9542
|
{
|
|
9538
9543
|
"path": "scripts/gds/ship-honesty.js",
|
|
@@ -10187,7 +10192,7 @@
|
|
|
10187
10192
|
{
|
|
10188
10193
|
"path": "src/module-api.js",
|
|
10189
10194
|
"mode": "0000644",
|
|
10190
|
-
"sha256": "
|
|
10195
|
+
"sha256": "19be294e50fce302cf4663c7776dfd8472eaddfb33805baf32b681603271cb77"
|
|
10191
10196
|
},
|
|
10192
10197
|
{
|
|
10193
10198
|
"path": "src/module-loader/catalog.js",
|
|
@@ -12037,7 +12042,7 @@
|
|
|
12037
12042
|
{
|
|
12038
12043
|
"path": "tests/grader_reader_lens.mjs",
|
|
12039
12044
|
"mode": "0000644",
|
|
12040
|
-
"sha256": "
|
|
12045
|
+
"sha256": "cacfafb2e8408bfcc2137ab1bf11bed21cf95c720aa0a7bc5cdbf0386f4833c8"
|
|
12041
12046
|
},
|
|
12042
12047
|
{
|
|
12043
12048
|
"path": "tests/grader_regrade_delta.mjs",
|
|
@@ -2446,5 +2446,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2446
2446
|
landed since 1.19.984 with no explicit bump. run 35902000024. (task 1002620)
|
|
2447
2447
|
1.19.986 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2448
2448
|
landed since 1.19.985 with no explicit bump. run 35903379073. (task 1002620)
|
|
2449
|
+
1.19.987 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2450
|
+
landed since 1.19.986 with no explicit bump. run 35906802480. (task 1002620)
|
|
2449
2451
|
---------------------------------------------------------------------------
|
|
2450
2452
|
```
|
|
@@ -126,15 +126,30 @@ const COPY_VISUAL_MODULES = Object.freeze([
|
|
|
126
126
|
'art-pipeline', // generated visual assets
|
|
127
127
|
'status-ui', // the public status dashboard
|
|
128
128
|
'ui-design', // the design-system surfaces (ADR 0081)
|
|
129
|
+
'public-landing', // the apex and the project hub (projects.html) — the owner-facing pages
|
|
129
130
|
]);
|
|
130
131
|
|
|
132
|
+
// A path that IS a rendered surface: a page, sheet, script or image under a
|
|
133
|
+
// module's public/ dir. Explicit for the same reason as the list above — a
|
|
134
|
+
// backend file in a UI module (routes/, db.js, module.json) is not visual work,
|
|
135
|
+
// and neither is a kit states file (.json).
|
|
136
|
+
const UI_SURFACE_PATH = /^modules\/[^/]+\/public\/.+\.(html?|css|m?js|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp4|webm)$/i;
|
|
137
|
+
function isUiSurfacePath(p) { return typeof p === 'string' && UI_SURFACE_PATH.test(p); }
|
|
138
|
+
|
|
131
139
|
// Does this shipped task's work put copy or visuals in front of a person? Pure
|
|
132
140
|
// and total — the runner treats a throw as "no match", but this must not throw.
|
|
133
|
-
|
|
141
|
+
//
|
|
142
|
+
// `changedFiles` is what the work actually CHANGED (task 1004201). The label
|
|
143
|
+
// alone missed most UI work: of 19 recent ships that changed page files, 14 had
|
|
144
|
+
// module_key null — the goal forest (task 1003480) shipped its overflowing
|
|
145
|
+
// titles with no visual lens and no review. Owner decision 2026-09-23: every UI
|
|
146
|
+
// change gets a visual review, and this is the predicate that files it.
|
|
147
|
+
function hasCopyOrVisualWork(task, changedFiles) {
|
|
134
148
|
if (!task) return false;
|
|
135
149
|
if (task.source === 'copy-proposal') return true; // a copy change that landed via the copy desk (ADR 0233)
|
|
136
150
|
if (task.discipline === 'artist') return true; // work an artist did is, by definition, on their surface
|
|
137
151
|
if (task.module_key && COPY_VISUAL_MODULES.includes(task.module_key)) return true;
|
|
152
|
+
if (Array.isArray(changedFiles) && changedFiles.some(isUiSurfacePath)) return true;
|
|
138
153
|
return false;
|
|
139
154
|
}
|
|
140
155
|
|
|
@@ -235,8 +250,12 @@ const CASCADES = Object.freeze([
|
|
|
235
250
|
// is read from ctx, never from branding, so the predicate stays pure; an
|
|
236
251
|
// absent ctx (a caller that resolved no policy) files, because the gate's
|
|
237
252
|
// default is 'strict' and silence must not be read as an opt-out.
|
|
253
|
+
//
|
|
254
|
+
// A changed-files list that hit GitHub's read cap FILES (task 1004201): a UI
|
|
255
|
+
// file may sit past the cap, and a padded PR must not skip its review.
|
|
238
256
|
when: (t, ctx) => (ctx || {}).artistGate !== 'off'
|
|
239
|
-
&& t.source !== CASCADE_SOURCE
|
|
257
|
+
&& t.source !== CASCADE_SOURCE
|
|
258
|
+
&& (hasCopyOrVisualWork(t, (ctx || {}).changedFiles) || (ctx || {}).changedFilesTruncated === true),
|
|
240
259
|
title: (t) => `Artist review: the copy and visuals that shipped in task ${t.id}`,
|
|
241
260
|
body: artistReviewBody,
|
|
242
261
|
}),
|
|
@@ -539,6 +558,17 @@ async function runCascadesForShip(shipped, deps = {}) {
|
|
|
539
558
|
try { ctx.artistGate = deps.artistGateMode(); }
|
|
540
559
|
catch (e) { report.errors.push(`cascade: artistGateMode() threw (${e && e.message}) — policy unread`); }
|
|
541
560
|
}
|
|
561
|
+
// WHAT THIS SHIP CHANGED (task 1004201), from the server's own view of the
|
|
562
|
+
// merged pull request, so the artist review keys on the work rather than a
|
|
563
|
+
// module_key label. A resolver that throws is reported and leaves the key
|
|
564
|
+
// unset: the label-based match still stands and the ship costs nothing.
|
|
565
|
+
if (typeof deps.changedFiles === 'function') {
|
|
566
|
+
try {
|
|
567
|
+
const changed = await deps.changedFiles(task);
|
|
568
|
+
ctx.changedFiles = (changed && changed.paths) || [];
|
|
569
|
+
ctx.changedFilesTruncated = Boolean(changed && changed.truncated);
|
|
570
|
+
} catch (e) { report.errors.push(`cascade: changedFiles() threw (${e && e.message}) — matched on labels only`); }
|
|
571
|
+
}
|
|
542
572
|
// WHAT THIS SHIP DELETED (task 1004090 / BV2.R15, ADR 0313). Resolved here
|
|
543
573
|
// rather than in the rule for the reason the rule's own comment gives: a
|
|
544
574
|
// `when` is pure and synchronous, and this is a network read.
|
|
@@ -30,6 +30,7 @@ const db = require('../db');
|
|
|
30
30
|
const cascade = require('../cascade');
|
|
31
31
|
const removalDerivation = require('../removal-derivation');
|
|
32
32
|
const removalTargets = require('../removal-targets');
|
|
33
|
+
const shipFiles = require('../ship-files');
|
|
33
34
|
|
|
34
35
|
// task 1003549 — the cross-role cascade listener. `task.shipped` already exists
|
|
35
36
|
// as a kernel event (emitted post-commit by the publish reconciler and the ship
|
|
@@ -70,49 +71,60 @@ function registerSeams() {
|
|
|
70
71
|
log: api.logger ? api.logger('lifecycle') : null,
|
|
71
72
|
}));
|
|
72
73
|
|
|
73
|
-
_cascadeListenerOff = api.on('task.shipped', (shipped) =>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
74
|
+
_cascadeListenerOff = api.on('task.shipped', (shipped) => {
|
|
75
|
+
// ONE pair of GitHub reads (find the PR, list its files) per ship, shared by
|
|
76
|
+
// the two resolvers below that both need them (task 1004201).
|
|
77
|
+
const pr = shipFiles.sharedPrReads();
|
|
78
|
+
return cascade.runCascadesForShip(shipped, {
|
|
79
|
+
getTask: (id) => db.getTask(id),
|
|
80
|
+
findTaskBySourceRef: (ref) => db.findTaskBySourceRef(ref),
|
|
81
|
+
createTask: (spec) => db.createTask(spec),
|
|
82
|
+
// The project's artist gate (ADR 0241, task 1003575). Resolved through the
|
|
83
|
+
// doorway's one reader rather than from branding().project, so the level that
|
|
84
|
+
// decides whether a review is FILED and the level that decides whether one
|
|
85
|
+
// HOLDS a deploy can never be read differently. At 'off' the artist-review
|
|
86
|
+
// rule files nothing.
|
|
87
|
+
artistGateMode: () => api.artistGate.artistGateMode(),
|
|
88
|
+
// WHAT THIS SHIP CHANGED (task 1004201): the merged PR's paths, so a UI
|
|
89
|
+
// change files its artist review whatever its module_key label says. The
|
|
90
|
+
// NAMED function, not a closure, for the reason removalCandidates gives below;
|
|
91
|
+
// the arrow only hands it this ship's shared reads.
|
|
92
|
+
changedFiles: (task) => shipFiles.changedFilesForTask(task, pr),
|
|
93
|
+
// WHAT THIS SHIP DELETED (task 1004090 / BV2.R15, ADR 0313). The derivation
|
|
94
|
+
// asks GitHub what the pull request actually changed and blames the deleted
|
|
95
|
+
// paths back through `claims.published_head_sha` — the SERVER's own view of
|
|
96
|
+
// the merge, never the client-supplied `diff_stats` the net-negative bonus
|
|
97
|
+
// reads, which is target-less and already known-forgeable.
|
|
98
|
+
//
|
|
99
|
+
// Resolving the PR number here rather than inside the derivation keeps that
|
|
100
|
+
// file free of the task->PR question, which github-push already answers.
|
|
101
|
+
// A task with no findable PR derives nothing, which files nothing.
|
|
102
|
+
//
|
|
103
|
+
// Every failure is swallowed into an empty result: this runs on the ship
|
|
104
|
+
// path, and ADR 0313 is explicit that landing work matters more than dimming
|
|
105
|
+
// a star on time.
|
|
106
|
+
//
|
|
107
|
+
// The resolver itself is `removalCandidatesForTask`, NOT a closure here. It
|
|
108
|
+
// was a closure, and it read the wrong field off findTaskPullRequest
|
|
109
|
+
// (`number` for `pr_number`), which made the whole rule dead in production
|
|
110
|
+
// while every test passed — because every test injected this dep. A closure
|
|
111
|
+
// inside a listener registration is not reachable from a test; a named
|
|
112
|
+
// function is, and this line is now too small to hide a bug of its own.
|
|
113
|
+
removalCandidates: (task) => removalDerivation.removalCandidatesForTask(task, {
|
|
114
|
+
...pr,
|
|
115
|
+
shippedTasksByPublishedSha: (shas) => db.shippedTasksByPublishedSha(shas),
|
|
116
|
+
}),
|
|
117
|
+
// WHOSE WORK IT WAS, stored (task 1004099 / BV2.R16 brightness). The
|
|
118
|
+
// derivation above already computed the attribution and the proposal body
|
|
119
|
+
// renders it as prose; this persists the same objects so brightness can
|
|
120
|
+
// read them structured. Passed as the NAMED function rather than wrapped in
|
|
121
|
+
// a closure, for the reason the note above gives: a closure here is not
|
|
122
|
+
// reachable from a test, and that is how the `number`/`pr_number` bug lived
|
|
123
|
+
// in production with a green suite.
|
|
124
|
+
recordRemovalTargets: removalTargets.recordRemovalTargets,
|
|
125
|
+
log: api.logger ? api.logger('lifecycle') : null,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
116
128
|
}
|
|
117
129
|
|
|
118
130
|
module.exports = function buildLifecycleRouter() {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/lifecycle/ship-files.js — the paths a shipped task's pull request
|
|
4
|
+
// changed, from the SERVER's own view of the merge (task 1004201).
|
|
5
|
+
//
|
|
6
|
+
// The artist-review cascade asks "did this ship change a rendered surface?" by
|
|
7
|
+
// the files, because the module_key label it used to key on is usually null.
|
|
8
|
+
// The answer comes from GitHub — the same two reads removal-derivation.js makes,
|
|
9
|
+
// SHARED with it per ship through sharedPrReads — never from a list the building
|
|
10
|
+
// box supplied. A task with no findable pull request changed nothing this can
|
|
11
|
+
// see, and answers no paths.
|
|
12
|
+
|
|
13
|
+
const githubPush = require('./github-push');
|
|
14
|
+
|
|
15
|
+
// github-push.getPullRequestFiles reads at most 3 pages of 100 and says nothing
|
|
16
|
+
// when it stops there, so a list this long may have been cut off. Pinned against
|
|
17
|
+
// that file by test.
|
|
18
|
+
const PR_FILES_CAP = 300;
|
|
19
|
+
|
|
20
|
+
// sharedPrReads — the two GitHub reads behind a ship's pull request, memoised for
|
|
21
|
+
// ONE ship event, so this file and removal-derivation.js ask GitHub once between
|
|
22
|
+
// them. Create one per event; never share it across ships.
|
|
23
|
+
function sharedPrReads(deps = {}) {
|
|
24
|
+
const find = deps.findTaskPullRequest || ((args) => githubPush.findTaskPullRequest(args));
|
|
25
|
+
const list = deps.getPullRequestFiles || ((args) => githubPush.getPullRequestFiles(args));
|
|
26
|
+
const memo = new Map();
|
|
27
|
+
const once = (key, read) => {
|
|
28
|
+
if (!memo.has(key)) memo.set(key, read());
|
|
29
|
+
return memo.get(key);
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
findTaskPullRequest: (args) => once(`pr:${args && args.taskId}`, () => find(args)),
|
|
33
|
+
getPullRequestFiles: (args) => once(`files:${args && args.number}`, () => list(args)),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// changedFilesForTask — { paths, truncated } for the task's pull request.
|
|
38
|
+
// `truncated` means the list hit the read cap, so a UI file may sit past it: the
|
|
39
|
+
// review rule files anyway rather than let a padded PR skip it. Throws on a failed
|
|
40
|
+
// read; the cascade runner reports that and falls back to the label match.
|
|
41
|
+
async function changedFilesForTask(task, deps = {}) {
|
|
42
|
+
const findPr = deps.findTaskPullRequest || ((args) => githubPush.findTaskPullRequest(args));
|
|
43
|
+
const getFiles = deps.getPullRequestFiles || ((args) => githubPush.getPullRequestFiles(args));
|
|
44
|
+
const pr = await findPr({ taskId: task && task.id });
|
|
45
|
+
// `pr_number`, NOT `pr.number` — findTaskPullRequest returns { branch, pr_number,
|
|
46
|
+
// pr_state, merged }, and reading the wrong field is silent (removal-derivation.js
|
|
47
|
+
// shipped dead in production over exactly this).
|
|
48
|
+
if (!pr || pr.pr_number == null) return { paths: [], truncated: false };
|
|
49
|
+
const files = await getFiles({ number: Number(pr.pr_number) });
|
|
50
|
+
const rows = Array.isArray(files) ? files : [];
|
|
51
|
+
return {
|
|
52
|
+
paths: rows.map((f) => f && f.path).filter((p) => typeof p === 'string' && p),
|
|
53
|
+
truncated: rows.length >= PR_FILES_CAP,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { PR_FILES_CAP, sharedPrReads, changedFilesForTask };
|
|
@@ -329,5 +329,152 @@ await test("runCascadesForShip honours 'off' end to end — nothing is created,
|
|
|
329
329
|
assert.deepEqual(report.errors, [], 'a project that switched the gate off has no problem to report');
|
|
330
330
|
});
|
|
331
331
|
|
|
332
|
+
// --- the review keys on the FILES a ship changed, not its label (task 1004201) --
|
|
333
|
+
|
|
334
|
+
await test('a page file changed under modules/*/public/ is visual work whatever the label says', () => {
|
|
335
|
+
const H = cascade.hasCopyOrVisualWork;
|
|
336
|
+
const unlabelled = shippedCopy({ module_key: null, discipline: 'engineer' });
|
|
337
|
+
assert.equal(H(unlabelled), false, 'the label alone says no — the gap task 1003480 fell through');
|
|
338
|
+
for (const p of [
|
|
339
|
+
'modules/hall-ui/public/goal-forest-lib.js',
|
|
340
|
+
'modules/public-landing/public/projects.html',
|
|
341
|
+
'modules/status-ui/public/status.css',
|
|
342
|
+
'modules/public-landing/public/assets/world/planet.webp',
|
|
343
|
+
]) assert.equal(H(unlabelled, ['README.md', p]), true, p);
|
|
344
|
+
assert.equal(H(shippedCopy({ module_key: 'public-landing' })), true, 'public-landing joined the label list');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await test('a backend file in a UI module is NOT visual work — page files only', () => {
|
|
348
|
+
const H = cascade.hasCopyOrVisualWork;
|
|
349
|
+
const unlabelled = shippedCopy({ module_key: null, discipline: 'engineer' });
|
|
350
|
+
assert.equal(H(unlabelled, [
|
|
351
|
+
'modules/hall-ui/module.json',
|
|
352
|
+
'modules/public-landing/routes/hub.js',
|
|
353
|
+
'modules/hall-ui/public/goals.states.json',
|
|
354
|
+
'modules/lifecycle/cascade.js',
|
|
355
|
+
'public-docs/index.html',
|
|
356
|
+
]), false);
|
|
357
|
+
assert.equal(H(unlabelled, []), false);
|
|
358
|
+
assert.equal(H(unlabelled, null), false);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
await test('the ship runner resolves the changed files ONCE and the artist review files on them', async () => {
|
|
362
|
+
const created = [];
|
|
363
|
+
let asked = 0;
|
|
364
|
+
const deps = {
|
|
365
|
+
getTask: async () => shippedCopy({ module_key: null, discipline: 'engineer' }),
|
|
366
|
+
findTaskBySourceRef: async () => null,
|
|
367
|
+
createTask: async (spec) => { created.push(spec); return { id: String(9300 + created.length) }; },
|
|
368
|
+
changedFiles: async () => { asked++; return { paths: ['modules/hall-ui/public/goal-forest-render.js'], truncated: false }; },
|
|
369
|
+
};
|
|
370
|
+
const report = await cascade.runCascadesForShip({ id: '7001' }, deps);
|
|
371
|
+
assert.equal(asked, 1);
|
|
372
|
+
assert.equal(created.filter((s) => s.discipline === 'artist').length, 1, 'the unlabelled UI ship gets its review');
|
|
373
|
+
assert.deepEqual(report.errors, []);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
await test('a changed-files list cut off at the read cap FILES the review — a padded PR cannot skip it', async () => {
|
|
377
|
+
const run = async (changed) => {
|
|
378
|
+
const created = [];
|
|
379
|
+
await cascade.runCascadesForShip({ id: '7001' }, {
|
|
380
|
+
getTask: async () => shippedCopy({ module_key: null, discipline: 'engineer' }),
|
|
381
|
+
findTaskBySourceRef: async () => null,
|
|
382
|
+
createTask: async (spec) => { created.push(spec); return { id: '9500' }; },
|
|
383
|
+
changedFiles: async () => changed,
|
|
384
|
+
});
|
|
385
|
+
return created.filter((s) => s.discipline === 'artist').length;
|
|
386
|
+
};
|
|
387
|
+
const padding = Array.from({ length: 300 }, (_, i) => `docs/pad-${i}.md`);
|
|
388
|
+
assert.equal(await run({ paths: padding, truncated: true }), 1);
|
|
389
|
+
assert.equal(await run({ paths: padding, truncated: false }), 0, 'the same list, complete, is not UI work');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
await test('a filed review is one the deploy gate HOLDS on — the rule and the gate agree on its identity', async () => {
|
|
393
|
+
const gate = require_(path.join(HERE, '..', '..', '..', 'src', 'bongos', 'artist-gate.js'));
|
|
394
|
+
const created = [];
|
|
395
|
+
await cascade.runCascadesForShip({ id: '7001' }, {
|
|
396
|
+
getTask: async () => shippedCopy({ module_key: null, discipline: 'engineer' }),
|
|
397
|
+
findTaskBySourceRef: async () => null,
|
|
398
|
+
createTask: async (spec) => { created.push(spec); return { id: '9600' }; },
|
|
399
|
+
changedFiles: async () => ({ paths: ['modules/public-landing/public/projects.html'], truncated: false }),
|
|
400
|
+
});
|
|
401
|
+
const review = created.find((s) => s.discipline === 'artist');
|
|
402
|
+
assert.ok(review, 'the review was filed');
|
|
403
|
+
assert.equal(gate.isArtistReview({ source: review.source, source_ref: review.sourceRef }), true);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
await test('a changed-files read that fails is REPORTED, never thrown — the label match still stands', async () => {
|
|
407
|
+
const run = async (task) => {
|
|
408
|
+
const created = [];
|
|
409
|
+
const report = await cascade.runCascadesForShip({ id: '7001' }, {
|
|
410
|
+
getTask: async () => task,
|
|
411
|
+
findTaskBySourceRef: async () => null,
|
|
412
|
+
createTask: async (spec) => { created.push(spec); return { id: '9400' }; },
|
|
413
|
+
changedFiles: async () => { throw new Error('github down'); },
|
|
414
|
+
});
|
|
415
|
+
return { created, report };
|
|
416
|
+
};
|
|
417
|
+
const unlabelled = await run(shippedCopy({ module_key: null, discipline: 'engineer' }));
|
|
418
|
+
assert.equal(unlabelled.created.length, 0);
|
|
419
|
+
assert.match(unlabelled.report.errors.join('\n'), /changedFiles\(\) threw \(github down\)/);
|
|
420
|
+
const labelled = await run(shippedCopy({ module_key: 'hall-ui' }));
|
|
421
|
+
assert.equal(labelled.created.filter((s) => s.discipline === 'artist').length, 1, 'an outage never costs a labelled review');
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const shipFiles = require_(path.join(HERE, '..', 'ship-files.js'));
|
|
425
|
+
|
|
426
|
+
await test('ship-files: reads the merged PR by pr_number and returns its paths', async () => {
|
|
427
|
+
const asked = [];
|
|
428
|
+
const changed = await shipFiles.changedFilesForTask({ id: '7001' }, {
|
|
429
|
+
findTaskPullRequest: async (a) => { asked.push(a); return { branch: 'task-7001', pr_number: 42, merged: true }; },
|
|
430
|
+
getPullRequestFiles: async ({ number }) => (number === 42
|
|
431
|
+
? [{ path: 'modules/hall-ui/public/a.css', status: 'modified' }, { path: 'modules/hall-ui/public/b.js', status: 'removed', deleted: true }, null]
|
|
432
|
+
: []),
|
|
433
|
+
});
|
|
434
|
+
assert.deepEqual(asked, [{ taskId: '7001' }]);
|
|
435
|
+
assert.deepEqual(changed, { paths: ['modules/hall-ui/public/a.css', 'modules/hall-ui/public/b.js'], truncated: false });
|
|
436
|
+
const noPr = await shipFiles.changedFilesForTask({ id: '7001' }, {
|
|
437
|
+
findTaskPullRequest: async () => ({ number: 42 }), // the WRONG field — must not be read as a PR
|
|
438
|
+
getPullRequestFiles: async () => { throw new Error('must not be called'); },
|
|
439
|
+
});
|
|
440
|
+
assert.deepEqual(noPr, { paths: [], truncated: false });
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
await test('ship-files: a list at the read cap says it may be cut off, and the cap is github-push\'s own', async () => {
|
|
444
|
+
const full = await shipFiles.changedFilesForTask({ id: '1' }, {
|
|
445
|
+
findTaskPullRequest: async () => ({ pr_number: 7 }),
|
|
446
|
+
getPullRequestFiles: async () => Array.from({ length: shipFiles.PR_FILES_CAP }, (_, i) => ({ path: `docs/${i}.md` })),
|
|
447
|
+
});
|
|
448
|
+
assert.equal(full.truncated, true);
|
|
449
|
+
const src = require_('fs').readFileSync(path.join(HERE, '..', 'github-push.js'), 'utf8');
|
|
450
|
+
const body = src.slice(src.indexOf('async function getPullRequestFiles'));
|
|
451
|
+
const pages = Number((body.match(/page <= (\d+)/) || [])[1]);
|
|
452
|
+
const perPage = Number((body.match(/per_page=(\d+)/) || [])[1]);
|
|
453
|
+
assert.equal(pages * perPage, shipFiles.PR_FILES_CAP, 'if getPullRequestFiles reads more, the cap must follow');
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
await test('ship-files: one ship asks GitHub ONCE between the two resolvers that need the PR', async () => {
|
|
457
|
+
let finds = 0;
|
|
458
|
+
let lists = 0;
|
|
459
|
+
const pr = shipFiles.sharedPrReads({
|
|
460
|
+
findTaskPullRequest: async () => { finds++; return { pr_number: 9 }; },
|
|
461
|
+
getPullRequestFiles: async () => { lists++; return [{ path: 'modules/hall-ui/public/x.css' }]; },
|
|
462
|
+
});
|
|
463
|
+
await shipFiles.changedFilesForTask({ id: '5' }, pr);
|
|
464
|
+
await pr.findTaskPullRequest({ taskId: '5' });
|
|
465
|
+
await pr.getPullRequestFiles({ number: 9 });
|
|
466
|
+
assert.equal(finds, 1);
|
|
467
|
+
assert.equal(lists, 1);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
await test('the ship listener wires the shared reads into BOTH resolvers', () => {
|
|
471
|
+
const fs = require_('fs');
|
|
472
|
+
const src = fs.readFileSync(path.join(HERE, '..', 'routes', 'lifecycle.js'), 'utf8');
|
|
473
|
+
assert.match(src, /require\('\.\.\/ship-files'\)/);
|
|
474
|
+
assert.match(src, /const pr = shipFiles\.sharedPrReads\(\);/, 'made per ship event, inside the listener');
|
|
475
|
+
assert.match(src, /changedFiles:\s*\(task\)\s*=>\s*shipFiles\.changedFilesForTask\(task, pr\)/);
|
|
476
|
+
assert.match(src, /removalDerivation\.removalCandidatesForTask\(task, \{\s*\.\.\.pr,/);
|
|
477
|
+
});
|
|
478
|
+
|
|
332
479
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
333
480
|
process.exit(failed ? 1 : 0);
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.987",
|
|
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.987",
|
|
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.987",
|
|
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",
|
|
@@ -296,13 +296,13 @@ async function runSubagentGrader({ task, valueSummary, baseline, changedFiles: p
|
|
|
296
296
|
// task 1003977: the advisory Reader rides along on copy-bearing diffs only.
|
|
297
297
|
// Where the predicate says no, `panel` is byte-identical to DEFAULT_PANEL
|
|
298
298
|
// and the ship costs exactly what it costs today.
|
|
299
|
-
const copyBearing = hasCopyOrVisualWork(task);
|
|
299
|
+
const copyBearing = hasCopyOrVisualWork(task, changedFiles);
|
|
300
300
|
const panel = grader.WORKERS.panelFor({ copyBearing });
|
|
301
301
|
console.log(` subagent grader ${grader.VERSION}: panel of ${panel.length} workers (${panel.join(', ')}), model=${grader.selectGraderModel({ builderModel })} (builder ${builderModel || `${grader.ASSUMED_BUILDER_MODEL} assumed`}), changed_files=${changedFiles.length}, diff_chars=${diff.length}, per-worker timeout=${GRADER_TIMEOUT_MS}ms`);
|
|
302
302
|
console.log(` gate: every GATING worker must return verdict='pass'; 'concerns' → manual-confirm fall-through; 'fail' blocks`);
|
|
303
303
|
console.log(copyBearing
|
|
304
304
|
? ` copy lens: ON — this task is copy-bearing, so the advisory 'reader' reads the changed strings against docs/voice.md. It is NON-GATING: nothing it reports, and no error or timeout it hits, can stop this ship.`
|
|
305
|
-
: ` copy lens: off — not a copy-bearing task (no copy-proposal source, not artist-discipline, module not in COPY_VISUAL_MODULES), so the panel is the usual four.`);
|
|
305
|
+
: ` copy lens: off — not a copy-bearing task (no copy-proposal source, not artist-discipline, module not in COPY_VISUAL_MODULES, no page file under modules/*/public/ changed), so the panel is the usual four.`);
|
|
306
306
|
if (priorRound) {
|
|
307
307
|
console.log(` delta re-grade (task 1002662): prior round's findings + builder response ride along, fenced; answered findings can only re-veto with NEW evidence`);
|
|
308
308
|
}
|
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.987'; // 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');
|
|
@@ -242,6 +242,13 @@ test('the cascade and the copy lens cover the SAME set by construction', () => {
|
|
|
242
242
|
const shipSrc = fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'ship-grade.js'), 'utf8');
|
|
243
243
|
assert.match(cascadeSrc, /hasCopyOrVisualWork/);
|
|
244
244
|
assert.match(shipSrc, /hasCopyOrVisualWork/);
|
|
245
|
+
// Task 1004201: BOTH consumers hand the predicate the files the work changed,
|
|
246
|
+
// so an unlabelled UI ship gets the lens at grade time AND the review on ship.
|
|
247
|
+
assert.match(shipSrc, /hasCopyOrVisualWork\(task, changedFiles\)/, 'the grader passes its own diff');
|
|
248
|
+
assert.match(cascadeSrc, /hasCopyOrVisualWork\(t, \(ctx \|\| \{\}\)\.changedFiles\)/, 'the cascade passes the merged PR');
|
|
249
|
+
const unlabelled = { module_key: null, discipline: 'engineer' };
|
|
250
|
+
assert.equal(hasCopyOrVisualWork(unlabelled, ['modules/hall-ui/public/goal-forest-lib.js']), true);
|
|
251
|
+
assert.equal(hasCopyOrVisualWork(unlabelled, ['modules/hall-ui/module.json', 'scripts/gds/claim.js']), false);
|
|
245
252
|
});
|
|
246
253
|
|
|
247
254
|
// ---- 8. COST ---------------------------------------------------------------
|