@bongos/core 1.19.1065 → 1.19.1067

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.
@@ -21,7 +21,9 @@
21
21
  // mechanism, its idempotency and its non-blocking property are proved by the
22
22
  // tests' own declarations, and R04 (task 1003115) writes the first production
23
23
  // entry. A mechanism whose first real rule is written by the task that needs it
24
- // is honest; one that ships a rule nobody asked for is a guess.
24
+ // is honest; one that ships a rule nobody asked for is a guess. (R04's artist
25
+ // review has since been removed by Tweak Mode, TW14; the removal proposal and
26
+ // the idea cascade remain. See the note above COPY_VISUAL_MODULES.)
25
27
  //
26
28
  // THE FOUR PROPERTIES THAT MAKE IT SAFE, each of which the tests pin:
27
29
  //
@@ -87,32 +89,34 @@
87
89
  // board has not triaged has, precisely, no classification yet.
88
90
  //
89
91
  // ---------------------------------------------------------------------------
90
- // The first production rule (R04 of goal 1000074, task 1003115).
92
+ // The artist review that USED to be the first rule (removed, TW14).
91
93
  // ---------------------------------------------------------------------------
92
94
  //
93
- // THE ARTIST REVIEW. "Every shipped work with copy or visuals becomes a Review
94
- // task for an artist, in two steps" (the criterion artist-review-task-on-ship).
95
- // This is the rule that makes that true, and it is the whole of R04's build: the
96
- // mechanism (ADR 0242) and its four safety properties already exist and are
97
- // tested; R04 writes the one declaration.
95
+ // R04 (task 1003115) wrote the first production entry here: `artist-review-on-
96
+ // ship`, which filed a two-step artist review at backlog for every ship with
97
+ // copy or visuals. It is GONE (task 1004325 / BV2.TW14, ADR 0341 D9). The reviews
98
+ // were one per ship, and 34 of the 39 it filed were abandoned, 33 with no note.
99
+ // Tweak Mode replaced it: the PAGE is the unit an artist works on, and a ship
100
+ // that changes a page's words now WRITES NOTHING. The page reads "changed since
101
+ // last tweak" because drift is DERIVED (ADR 0341 D8, modules/copy-desk/
102
+ // page-status.js driftFor): the page's current reading against the lines its
103
+ // last round left. Nothing to store, so nothing here to file.
98
104
  //
99
- // TWO GUARDS THAT KEEP IT FROM MISFIRING, both pinned in the tests:
100
- // • It excludes `source === CASCADE_SOURCE`. Without that, the artist review
101
- // task it generates — discipline 'artist', so it matches its own predicate —
102
- // would on ship generate a review OF the review, and so on. The exclusion
103
- // makes the generation a single hop, never a chain.
104
- // • "Copy or visuals" is a NARROW, explicit predicate (hasCopyOrVisualWork),
105
- // not "every ship". A cascade that fired on everything would bury the artist,
106
- // and the criterion's "dismissable rather than nagging" would be a lie.
105
+ // Do not bring a per-ship artist rule back to "mark the page": the mark already
106
+ // exists, derived, and a stored one would be a second record of what the
107
+ // readings and the ledger say.
107
108
  //
108
- // It does NOT block anything (ADR 0241 draws the line: an artist may hold a
109
- // DEPLOY, decided per project, never a builder's ship; that gate is a separate
110
- // build). The generated task is a `backlog` request that triage may dismiss.
109
+ // What stays, and why:
110
+ // • `hasCopyOrVisualWork` stays exported. The grader's advisory reader lens
111
+ // (ADR 0298, scripts/gds/ship-grade.js) is scoped by it.
112
+ // • The runner still resolves `ctx.artistGate` and `ctx.changedFiles` when a
113
+ // caller injects them (routes/lifecycle.js does). No production rule reads
114
+ // either today; the reads are shared with the removal rule's (ship-files.js
115
+ // memoises them), so they cost nothing, and ADR 0341 D9 keeps the runner.
116
+ // • Review rows already filed (source 'cascade', ref prefix `cascade-artist-
117
+ // review-on-ship-`) are ordinary data. The artist gate (ADR 0241) and the
118
+ // studio still read them; nothing deletes them.
111
119
 
112
- // The modules whose shipped work puts user-facing copy or visuals in front of a
113
- // person. Kept as an explicit set rather than inferred, because "is this
114
- // copy/visual work" is a judgement the rule should state out loud and a test
115
- // should be able to pin — an inferred predicate is the kind that quietly widens.
116
120
  // The removal rule's key, lifted out of its declaration so the SURFACE that
117
121
  // answers a removal proposal (task 1004095 / BV2.R16) can find those proposals
118
122
  // without hand-typing the string. The key is half of every generated row's
@@ -120,6 +124,11 @@
120
124
  // reader that silently returns nothing the day the key changes.
121
125
  const REMOVAL_RULE_KEY = 'removal-proposed-on-ship';
122
126
 
127
+ // The modules whose shipped work puts user-facing copy or visuals in front of a
128
+ // person. Kept as an explicit set rather than inferred, because "is this
129
+ // copy/visual work" is a judgement the predicate should state out loud and a
130
+ // test should be able to pin — an inferred predicate is the kind that quietly
131
+ // widens.
123
132
  const COPY_VISUAL_MODULES = Object.freeze([
124
133
  'copy-desk', // the copy registry + proposals (goal 1000074's own spine)
125
134
  'hall-ui', // the builders' hall — the most-seen surface
@@ -142,8 +151,9 @@ function isUiSurfacePath(p) { return typeof p === 'string' && UI_SURFACE_PATH.te
142
151
  // `changedFiles` is what the work actually CHANGED (task 1004201). The label
143
152
  // alone missed most UI work: of 19 recent ships that changed page files, 14 had
144
153
  // 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.
154
+ // titles with no visual lens. Owner decision 2026-09-23: every UI change gets a
155
+ // visual lens. Since TW14 no cascade files a review on it; the grader's reader
156
+ // lens (ADR 0298) is what this predicate scopes.
147
157
  function hasCopyOrVisualWork(task, changedFiles) {
148
158
  if (!task) return false;
149
159
  if (task.source === 'copy-proposal') return true; // a copy change that landed via the copy desk (ADR 0233)
@@ -153,44 +163,6 @@ function hasCopyOrVisualWork(task, changedFiles) {
153
163
  return false;
154
164
  }
155
165
 
156
- // The two-step artist review body. STEP 1 records where the grade was blind;
157
- // STEP 2 remakes the copy/visuals with both routes explicitly offered — R04's
158
- // brief, verbatim in intent.
159
- //
160
- // STEP 1 ASKS FOR COACHING AGAIN, because there is now a lens listening.
161
- //
162
- // The history, because the wording is load-bearing: STEP 1 originally said
163
- // "coach the grader", which asked for something no artist could do (task
164
- // 1003937, ADR 0298) — the panel's four lenses read completion, code quality,
165
- // security and runtime cost, and not one read a sentence, so four consecutive
166
- // reviews found the same wall. The step was rewritten to ask for the RECORD
167
- // instead, and that version carried an explicit instruction not to restore the
168
- // coaching wording early, because the wording is the signal for which world we
169
- // are in.
170
- //
171
- // Task 1003977 shipped the fifth lens: an ADVISORY 'reader', scoped to
172
- // copy-bearing diffs by THIS FILE'S hasCopyOrVisualWork — the same predicate
173
- // that fires this cascade, so an artist review and the copy lens cover exactly
174
- // the same set by construction, and widening one widens both. The voice guide
175
- // it waited on is docs/voice.md (task 1003976). So there is a counterpart to
176
- // coach, it has a written standard to be coached against, and the record the
177
- // interim wording collected is what both were built from.
178
- //
179
- // What has NOT changed: the reader is non-gating and cannot hold a ship (ADR
180
- // 0241 keeps the artist's judgement on the deploy), so this review is still a
181
- // request triage may dismiss.
182
- function artistReviewBody(task) {
183
- return [
184
- `This is an ARTIST REVIEW, generated automatically because task ${task.id} put copy or visuals in front of a person. It is a REQUEST, not a gate — it blocks nothing that shipped, and triage may dismiss it.`,
185
- '',
186
- `STEP 1 — COACH THE READER. Pull how the grader judged task ${task.id} (GET /tasks/${task.id}?include=grade) and find the 'reader' row in signals.workers, with its copy_score in rubric_scores. That lens read the changed copy against docs/voice.md and cited rule ids. Say where it was RIGHT, where it was WRONG, and — the valuable one — what it did not see at all. A finding it filed that you disagree with is worth as much as one it missed: both are corrections to a lens that is listening, which is what this step could not ask for before task 1003977 shipped it (ADR 0298). If the miss is because no rule in docs/voice.md reaches it, that is a gap in the GUIDE — file it against the guide rather than expecting the lens to invent a rule at grade time. Two cases where there will be no reader row and that is not a fault: a ship that took the trivial-diff path (a single generalist shot, no panel), and one where the lens errored — it is advisory, so an outage is recorded and nothing is blocked.`,
187
- '',
188
- 'STEP 2 — REMAKE OR TWEAK the copy/visuals if they need it. Your choice of route, both offered on purpose: OUTSIDE the AI chat (edit the surface directly and land it through the copy desk or the normal pipeline), or WITH it (draft in chat, then land). Either way it lands as a claimed, graded, shipped task like everything else — there is no live CMS (goal 1000074\'s hard rule).',
189
- '',
190
- 'If the copy and visuals are fine and there was nothing the grade missed, DISMISS this in triage. That is a real, expected outcome — not a failure.',
191
- ].join('\n');
192
- }
193
-
194
166
  // The removal proposal's body (task 1004090 / BV2.R15, ADR 0313).
195
167
  //
196
168
  // IT ASKS, IT DOES NOT TELL. Every line here is written so a reader confirms or
@@ -235,44 +207,25 @@ function removalProposalBody(task, ctx) {
235
207
  return lines.join('\n');
236
208
  }
237
209
 
238
- // Production rules. The artist review is the first (R04); add another by
239
- // appending an entry, never by touching the runner (ADR 0242).
210
+ // Production rules. Add one by appending an entry, never by touching the runner
211
+ // (ADR 0242). The artist review (R04) was the first and is gone (TW14, ADR 0341
212
+ // D9; see the note above COPY_VISUAL_MODULES), so the removal proposal is the
213
+ // only ship rule today.
240
214
  const CASCADES = Object.freeze([
241
- Object.freeze({
242
- key: 'artist-review-on-ship',
243
- discipline: 'artist',
244
- kind: 'unclassified',
245
- // The anti-recursion guard (source !== CASCADE_SOURCE) is FIRST so a
246
- // cascade-generated task is rejected before any other test runs.
247
- //
248
- // The project's artist_gate at 'off' means "not generated" (ADR 0241 §2's
249
- // table, task 1003575) — the one level at which this rule files nothing. It
250
- // is read from ctx, never from branding, so the predicate stays pure; an
251
- // absent ctx (a caller that resolved no policy) files, because the gate's
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.
256
- when: (t, ctx) => (ctx || {}).artistGate !== 'off'
257
- && t.source !== CASCADE_SOURCE
258
- && (hasCopyOrVisualWork(t, (ctx || {}).changedFiles) || (ctx || {}).changedFilesTruncated === true),
259
- title: (t) => `Artist review: the copy and visuals that shipped in task ${t.id}`,
260
- body: artistReviewBody,
261
- }),
262
215
  Object.freeze({
263
216
  key: REMOVAL_RULE_KEY,
264
217
  // ENGINEER, not artist: confirming asks whether deleted code is genuinely
265
218
  // gone or was moved, which is a question about the diff.
266
219
  discipline: 'engineer',
267
220
  kind: 'unclassified',
268
- // The anti-recursion guard FIRST, the same order and for the same reason as
269
- // the rule above — a cascade-generated task must never cascade.
221
+ // The anti-recursion guard FIRST: a cascade-generated task must never
222
+ // cascade, so it is rejected before any other test runs.
270
223
  //
271
224
  // The candidates are resolved ONCE into ctx by runCascadesForShip, never
272
225
  // here: a `when` is pure and synchronous by contract, and this derivation is
273
226
  // a network read. An absent ctx therefore means "nothing was derived" and
274
- // files nothing — the opposite default to the artist gate above, and
275
- // deliberately so: silence about a removal must never be read as a removal.
227
+ // files nothing, deliberately: silence about a removal must never be read
228
+ // as a removal.
276
229
  when: (t, ctx) => t.source !== CASCADE_SOURCE
277
230
  && Array.isArray((ctx || {}).removals) && (ctx || {}).removals.length > 0,
278
231
  title: (t) => `Was this removed? Task ${t.id} deleted earlier shipped work`,
@@ -418,9 +371,9 @@ function cascadesFor(task, rules = CASCADES, ctx = {}, trigger = TRIGGERS.ship)
418
371
 
419
372
  // The createTask payload for one matched rule. The attribution preamble is
420
373
  // prepended here rather than left to each rule, so no rule can forget it.
421
- // `ctx` reaches body() as a second argument, mirroring when(). The artist rule
422
- // ignores it; the removal rule needs it, because what a ship deleted is derived
423
- // per run and is not a property of the task row.
374
+ // `ctx` reaches body() as a second argument, mirroring when(). The removal rule
375
+ // needs it, because what a ship deleted is derived per run and is not a property
376
+ // of the task row.
424
377
  function cascadeTaskSpec(rule, task, trigger = TRIGGERS.ship, ctx = {}) {
425
378
  const body = String(rule.body(task, ctx) || '').trim();
426
379
  const preamble =
@@ -559,9 +512,9 @@ async function runCascadesForShip(shipped, deps = {}) {
559
512
  catch (e) { report.errors.push(`cascade: artistGateMode() threw (${e && e.message}) — policy unread`); }
560
513
  }
561
514
  // 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.
515
+ // merged pull request, so a rule can key on the work rather than a
516
+ // module_key label (the artist review did, until TW14 removed it). A resolver
517
+ // that throws is reported and leaves the key unset, and the ship costs nothing.
565
518
  if (typeof deps.changedFiles === 'function') {
566
519
  try {
567
520
  const changed = await deps.changedFiles(task);
@@ -650,7 +603,6 @@ module.exports = {
650
603
  COPY_VISUAL_MODULES,
651
604
  hasCopyOrVisualWork,
652
605
  isUiSurfacePath,
653
- artistReviewBody,
654
606
  cascadeSourceRef,
655
607
  assertRuleShape,
656
608
  validateCascades,
@@ -18,6 +18,7 @@ const doneWhen = require('./done-when.js');
18
18
  const { THETES_GRADUATION_THRESHOLD, maybeAutoGraduateToThetes, foldWorktreeName } = require('./db-rank-authz.js');
19
19
  const { mostRecentClaimHolderId, sealSessionLog, staleClaimHourPredicate } = require('./db-shared.js');
20
20
  const { LIVE_STATUSES } = require('./dead-deps.js');
21
+ const { isPageTweak } = require('./page-tweak-hold.js');
21
22
 
22
23
  // shouldRefuseClaimResolve — may THIS caller resolve THIS claim? (task 1003767)
23
24
  //
@@ -346,8 +347,15 @@ async function resolveClaim({ claimId, builderId, outcome, notesMd, valueSummary
346
347
  // row, re-landing books a FRESH 'task.shipped' row — the reward is fully
347
348
  // re-earnable, exactly as the ADR promises. Caller passes its ship txn client
348
349
  // so the credit commits atomically with the status flip.
349
- async function awardLandingCredit(client, { taskId, creditedBuilderId, creditsReward, kind }) {
350
+ async function awardLandingCredit(client, { taskId, creditedBuilderId, creditsReward, kind, source = null }) {
350
351
  const reward = seams.resolveOptional('reward');
352
+ // ADR 0341 D10 (task 1004324): a page tweak's per-task stream is its ARTIST's, on the
353
+ // artist lane, in place of the shipper's estimate (economy reads the payee); the
354
+ // applier keeps its cost-plus session reward, which is booked elsewhere.
355
+ if (reward && isPageTweak({ source })) {
356
+ const artist = reward.awardArtistPageCredit ? await reward.awardArtistPageCredit(client, { taskId }) : null;
357
+ return { creditsAwarded: 0, ideaBonus: null, artist };
358
+ }
351
359
  if (!reward || creditedBuilderId == null || !(Number(creditsReward) > 0)) {
352
360
  return { creditsAwarded: 0, ideaBonus: null };
353
361
  }
@@ -510,7 +518,7 @@ async function shipTask({ taskId, builderId, landAssurance }, deps = {}) {
510
518
  // under NODE_ENV==='test', so a regression test can drive the txn with a
511
519
  // SQL-aware fakeClient. Production callers omit deps → module pool.
512
520
  const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
513
- const { shipRow, graduationBuilder, rankGraduation, actualShipperId, closedClaimCount, creditedBuilderId, creditsAwarded, ideaBonus, justUnlocked } = await withTx(async (client) => {
521
+ const { shipRow, graduationBuilder, rankGraduation, actualShipperId, closedClaimCount, creditedBuilderId, creditsAwarded, ideaBonus, artistCredit, justUnlocked } = await withTx(async (client) => {
514
522
  const { rows } = await client.query(
515
523
  `UPDATE tasks
516
524
  SET status = 'shipped',
@@ -521,7 +529,7 @@ async function shipTask({ taskId, builderId, landAssurance }, deps = {}) {
521
529
  needs_rebase_at = NULL,
522
530
  updated_at = now()
523
531
  WHERE id = $1 AND status = 'confirmed'
524
- RETURNING id, title, status, shipped_at, shipped_by, security_sensitive, value_summary, credits_reward, kind`,
532
+ RETURNING id, title, status, shipped_at, shipped_by, security_sensitive, value_summary, credits_reward, kind, source`,
525
533
  [taskId, builderId]
526
534
  );
527
535
  if (rows.length === 0) {
@@ -626,6 +634,7 @@ async function shipTask({ taskId, builderId, landAssurance }, deps = {}) {
626
634
  creditedBuilderId,
627
635
  creditsReward: rows[0].credits_reward,
628
636
  kind: rows[0].kind,
637
+ source: rows[0].source,
629
638
  });
630
639
  // task 1679 pattern: state-based achievement reconcile now that the credit
631
640
  // (above) + the ship count are reflected in this txn. SAVEPOINT-wrapped so
@@ -647,7 +656,7 @@ async function shipTask({ taskId, builderId, landAssurance }, deps = {}) {
647
656
  return {
648
657
  shipRow: rows[0], graduationBuilder, rankGraduation, actualShipperId,
649
658
  closedClaimCount: closedClaims.length,
650
- creditedBuilderId, creditsAwarded: landing.creditsAwarded, ideaBonus: landing.ideaBonus, justUnlocked,
659
+ creditedBuilderId, creditsAwarded: landing.creditsAwarded, ideaBonus: landing.ideaBonus, artistCredit: landing.artist || null, justUnlocked,
651
660
  };
652
661
  }, { pool: activePool });
653
662
  if (closedClaimCount > 0) {
@@ -677,7 +686,7 @@ async function shipTask({ taskId, builderId, landAssurance }, deps = {}) {
677
686
  // ADR 0120: the per-task reward that landed on THIS ship (0 on a re-ship /
678
687
  // reconciler re-sweep — idempotent). The ship route surfaces it so the
679
688
  // builder sees "credits landed" at ship, not at confirm.
680
- creditedBuilderId, creditsAwarded, ideaBonus, justUnlocked,
689
+ creditedBuilderId, creditsAwarded, ideaBonus, artistCredit, justUnlocked,
681
690
  };
682
691
  }
683
692
 
@@ -79,16 +79,16 @@ function registerSeams() {
79
79
  getTask: (id) => db.getTask(id),
80
80
  findTaskBySourceRef: (ref) => db.findTaskBySourceRef(ref),
81
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.
82
+ // The project's artist gate (ADR 0241, task 1003575), resolved through the
83
+ // doorway's one reader rather than from branding().project. The artist-review
84
+ // rule that read it was removed by Tweak Mode (TW14, ADR 0341 D9), so no
85
+ // production rule reads it today; the runner still offers it as policy.
87
86
  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.
87
+ // WHAT THIS SHIP CHANGED (task 1004201): the merged PR's paths. No rule reads
88
+ // them since TW14, and they cost nothing extra: sharedPrReads memoises the
89
+ // same two GitHub reads removalCandidates makes. The NAMED function, not a
90
+ // closure, for the reason removalCandidates gives below; the arrow only hands
91
+ // it this ship's shared reads.
92
92
  changedFiles: (task) => shipFiles.changedFilesForTask(task, pr),
93
93
  // WHAT THIS SHIP DELETED (task 1004090 / BV2.R15, ADR 0313). The derivation
94
94
  // asks GitHub what the pull request actually changed and blames the deleted