@bongos/core 1.21.97 → 1.21.99

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.
@@ -0,0 +1,140 @@
1
+ -- core_274 a task a PERSON parked in backlog is never auto-promoted out of it
2
+ -- Applied under task 1004592.
3
+ --
4
+ -- WHY. Two routes put a task into backlog on purpose:
5
+ --
6
+ -- POST /tasks/:id/promote on an ABANDONED task restoreTask (ADR 0152) -- the
7
+ -- task "comes back un-workable and needs a second, explicit promote"
8
+ -- POST /tasks/:id/demote a person took a ready task
9
+ -- back off the board
10
+ --
11
+ -- Neither left anything behind in the row. The promotion sweep
12
+ -- (modules/lifecycle/db-tasks.js PROMOTABLE_BACKLOG_WHERE) promotes every
13
+ -- backlog row with no unmet dependency and no open blocker, so both were undone
14
+ -- on the next reconciler tick. OBSERVED on task 1003397 (2026-10-06): restored,
15
+ -- promoted by the sweep at 19:58:34Z with nobody promoting it; demoted at about
16
+ -- 20:00Z, back at ready at 20:03:34Z, one tick later.
17
+ --
18
+ -- SHAPE. A durable marker, tasks.parked_at + tasks.parked_by, written by the
19
+ -- two routes and honoured by every promoter:
20
+ --
21
+ -- (1) the sweep: `AND t.parked_at IS NULL` in PROMOTABLE_BACKLOG_WHERE
22
+ -- (2) the three trigger functions below, for the same reason core_250 gives
23
+ -- for its cascade clause: a parked task that HAS dependencies would
24
+ -- otherwise be promoted the moment one of them ships. Fixing only the
25
+ -- sweep would leave that path open.
26
+ --
27
+ -- The marker is cleared by a BEFORE UPDATE trigger whenever status leaves
28
+ -- 'backlog'. So a person's promote needs no change at all, and a stale marker
29
+ -- can never strand a task that a person later sends back to backlog by some
30
+ -- other route: once it leaves backlog, it is no longer parked.
31
+ --
32
+ -- NOT task_is_fully_unblocked(). Same reason as core_250: that predicate is
33
+ -- shared with the claim gate and answers "are the dependencies done", not "has
34
+ -- a person said wait".
35
+ --
36
+ -- Forward-safe: two nullable columns with no default, CREATE OR REPLACE only.
37
+ -- An older release never writes parked_at, so on a rolled-back pin nothing is
38
+ -- parked and promotion behaves exactly as before. The three function bodies are
39
+ -- core_250's verbatim apart from the one added line each.
40
+
41
+ ALTER TABLE tasks ADD COLUMN IF NOT EXISTS parked_at timestamptz;
42
+ ALTER TABLE tasks ADD COLUMN IF NOT EXISTS parked_by bigint REFERENCES builders(id) ON DELETE SET NULL;
43
+
44
+ -- Leaving backlog un-parks. Runs only when status actually changes, so a
45
+ -- write that parks a task (status -> 'backlog' plus parked_at) keeps it.
46
+ CREATE OR REPLACE FUNCTION clear_parked_on_leave_backlog()
47
+ RETURNS TRIGGER AS $$
48
+ BEGIN
49
+ IF NEW.status IS DISTINCT FROM 'backlog' THEN
50
+ NEW.parked_at := NULL;
51
+ NEW.parked_by := NULL;
52
+ END IF;
53
+ RETURN NEW;
54
+ END;
55
+ $$ LANGUAGE plpgsql;
56
+
57
+ DROP TRIGGER IF EXISTS trg_clear_parked_on_leave_backlog ON tasks;
58
+ CREATE TRIGGER trg_clear_parked_on_leave_backlog
59
+ BEFORE UPDATE OF status ON tasks
60
+ FOR EACH ROW
61
+ WHEN (OLD.status IS DISTINCT FROM NEW.status)
62
+ EXECUTE FUNCTION clear_parked_on_leave_backlog();
63
+
64
+ -- (a) a task shipped
65
+ CREATE OR REPLACE FUNCTION promote_dependents_on_ship()
66
+ RETURNS TRIGGER AS $$
67
+ BEGIN
68
+ IF NEW.status = 'shipped' AND (OLD.status IS DISTINCT FROM 'shipped') THEN
69
+ UPDATE tasks t
70
+ SET status = 'ready', promoted_at = COALESCE(t.promoted_at, now()), updated_at = now()
71
+ WHERE t.status = 'backlog'
72
+ AND t.kind <> 'spike'
73
+ AND t.source IS DISTINCT FROM 'cascade'
74
+ AND t.parked_at IS NULL
75
+ AND EXISTS (
76
+ SELECT 1 FROM dependencies d
77
+ WHERE d.to_kind = 'task' AND d.to_id = NEW.id
78
+ AND ( (d.from_kind = 'task' AND d.from_id = t.id)
79
+ OR (d.from_kind = 'goal' AND d.from_id = t.goal_id)
80
+ OR (d.from_kind = 'criterion' AND d.from_id IN (
81
+ SELECT tc.criterion_id FROM task_criteria tc WHERE tc.task_id = t.id)) )
82
+ )
83
+ AND task_is_fully_unblocked(t.id);
84
+ END IF;
85
+ RETURN NEW;
86
+ END;
87
+ $$ LANGUAGE plpgsql;
88
+
89
+ -- (b) a criterion satisfied
90
+ CREATE OR REPLACE FUNCTION promote_dependents_on_criterion_satisfy()
91
+ RETURNS TRIGGER AS $$
92
+ BEGIN
93
+ IF NEW.satisfied = true AND (OLD.satisfied IS DISTINCT FROM true) THEN
94
+ UPDATE tasks t
95
+ SET status = 'ready', promoted_at = COALESCE(t.promoted_at, now()), updated_at = now()
96
+ WHERE t.status = 'backlog'
97
+ AND t.kind <> 'spike'
98
+ AND t.source IS DISTINCT FROM 'cascade'
99
+ AND t.parked_at IS NULL
100
+ AND EXISTS (
101
+ SELECT 1 FROM dependencies d
102
+ WHERE d.to_kind = 'criterion' AND d.to_id = NEW.id
103
+ AND ( (d.from_kind = 'task' AND d.from_id = t.id)
104
+ OR (d.from_kind = 'goal' AND d.from_id = t.goal_id)
105
+ OR (d.from_kind = 'criterion' AND d.from_id IN (
106
+ SELECT tc.criterion_id FROM task_criteria tc WHERE tc.task_id = t.id)) )
107
+ )
108
+ AND task_is_fully_unblocked(t.id);
109
+ END IF;
110
+ RETURN NEW;
111
+ END;
112
+ $$ LANGUAGE plpgsql;
113
+
114
+ -- (c) a goal achieved
115
+ CREATE OR REPLACE FUNCTION promote_dependents_on_goal_achieve()
116
+ RETURNS TRIGGER AS $$
117
+ BEGIN
118
+ IF NEW.status = 'achieved' AND (OLD.status IS DISTINCT FROM 'achieved') THEN
119
+ UPDATE tasks t
120
+ SET status = 'ready', promoted_at = COALESCE(t.promoted_at, now()), updated_at = now()
121
+ WHERE t.status = 'backlog'
122
+ AND t.kind <> 'spike'
123
+ AND t.source IS DISTINCT FROM 'cascade'
124
+ AND t.parked_at IS NULL
125
+ AND EXISTS (
126
+ SELECT 1 FROM dependencies d
127
+ WHERE d.to_kind = 'goal' AND d.to_id = NEW.id
128
+ AND ( (d.from_kind = 'task' AND d.from_id = t.id)
129
+ OR (d.from_kind = 'goal' AND d.from_id = t.goal_id)
130
+ OR (d.from_kind = 'criterion' AND d.from_id IN (
131
+ SELECT tc.criterion_id FROM task_criteria tc WHERE tc.task_id = t.id)) )
132
+ )
133
+ AND task_is_fully_unblocked(t.id);
134
+ END IF;
135
+ RETURN NEW;
136
+ END;
137
+ $$ LANGUAGE plpgsql;
138
+
139
+ INSERT INTO schema_migrations (version) VALUES ('core_274_parked_backlog_stays_parked')
140
+ ON CONFLICT (version) DO NOTHING;
@@ -19,6 +19,10 @@
19
19
  // matchLine(text, lines) which line of the studio's own reading a word on
20
20
  // the page is, so an in-place edit names its key
21
21
  // claimWords(code, detail) a refused claim, in words
22
+ // skyAt(date) 'day' 07:00-18:59 on the viewer's clock, else 'night'
23
+ // dayPart(date) 'morning' / 'afternoon' in the day room, 'evening' at night
24
+ // clockGreets(served) whether the served greeting is still the stock one,
25
+ // so the clock may choose (an artist's tweak wins)
22
26
  //
23
27
  // Node-require-safe (the tweak-editor-lib.js pattern): assignment is the only
24
28
  // eval-time side effect, and only when a window exists.
@@ -198,9 +202,27 @@
198
202
  return 'That page could not be taken just now. Nothing changed.';
199
203
  }
200
204
 
205
+ // The room's sky and the greeting's time of day (task 1004654; the mockup
206
+ // docs/design/mocks/day-night-rooms). Day from 07:00 to 18:59 on the viewer's
207
+ // own clock, night otherwise: the creation page's rule (projects.html skyAt),
208
+ // written again here because hall-ui does not import the landing's code. The
209
+ // greeting is "morning" before noon in the day room, "afternoon" after it,
210
+ // and "evening" whenever the room is at night.
211
+ function skyAt(d) { const h = d.getHours(); return h >= 7 && h < 19 ? 'day' : 'night'; }
212
+ function dayPart(d) {
213
+ if (skyAt(d) === 'night') return 'evening';
214
+ return d.getHours() < 12 ? 'morning' : 'afternoon';
215
+ }
216
+ // The greeting the page is served with. Only while the page still says
217
+ // exactly this does the clock choose the greeting: once an artist's tweak of
218
+ // the studio page has rewritten it, their words show at every hour.
219
+ const STOCK_GREETING = 'Good evening.';
220
+ function clockGreets(served) { return tidy(served) === STOCK_GREETING; }
221
+
201
222
  const api = {
202
223
  STUDIO_PAGE, PAGE_ID_RE, SURFACE_WORDS, surfaceWord, titleOf, numberWords, tallyParts, resumeDetail,
203
224
  subParts, flowDetail, statusDetail, apexHost, landingHost, surfaceRows, askedLine, pickLabel, matchLine, claimWords,
225
+ skyAt, dayPart, STOCK_GREETING, clockGreets,
204
226
  };
205
227
  if (typeof window !== 'undefined') window.OTBStudioLib = api;
206
228
  if (typeof module !== 'undefined' && module.exports) module.exports = api;
@@ -17,6 +17,15 @@
17
17
  shell's full-screen button while it floats over the room. The hall chrome
18
18
  around the stage keeps its own light and dark.
19
19
 
20
+ ONE ROOM, TWO SKIES (task 1004654). By the viewer's own clock the room is
21
+ the night room or the day room of docs/design/mocks/day-night-rooms (day
22
+ from 07:00 to 18:59, re-checked each minute; studio.js sets data-sky on the
23
+ body). The day palette is the mockup's [data-sky="day"] block, declared just
24
+ below the night tokens it replaces; the same drawing, with clouds and a sun
25
+ in the window, lighter towers with unlit panes, a warm wall and floor, the
26
+ lamp off and a patch of sun on the floor. The glass panels over the room
27
+ follow its sky; the hall chrome still keeps its own light and dark.
28
+
20
29
  The second half is the older reviews' garden (R06 of goal 1000074, task
21
30
  1003117), which still answers the reviews filed before Tweak Mode (TW14) in
22
31
  its own quiet dialog, in the hall's own tokens as it always was. */
@@ -52,6 +61,9 @@ body[data-page="studio"] {
52
61
  --st-accent: #fb9569;
53
62
  --st-on-accent: #000000;
54
63
  --st-link: #fcbfa4;
64
+ --st-pane: #fb9569;
65
+ --st-credits: #fb9569;
66
+ --st-focus: #fb9569;
55
67
  --st-resume: rgba(251, 149, 105, .16);
56
68
  --st-resume-edge: rgba(251, 149, 105, .55);
57
69
  --st-word-hover: rgba(255, 255, 255, .28);
@@ -69,6 +81,86 @@ body[data-page="studio"] {
69
81
  --st-mono: var(--font-mono, "JetBrains Mono", ui-monospace, Consolas, monospace);
70
82
  }
71
83
 
84
+ /* The day room: the mockup's [data-sky="day"] tokens, on the same names. Three
85
+ inks are set darker than the mockup's so they hold AA on the glass over a
86
+ light wall (records/studio.md): the link, and the credits figure and focus
87
+ ring, which by night are the accent. */
88
+ body[data-page="studio"][data-sky="day"] {
89
+ --st-room: #efe7dc;
90
+ --st-frame: #d6c9b7;
91
+ --st-night: #bfdaf0;
92
+ --st-moon: #ffd46e;
93
+ --st-moon-halo: rgba(255, 196, 90, .5);
94
+ --st-star: #ffffff;
95
+ --st-tower-1: #9db2c7;
96
+ --st-tower-2: #8ea4bb;
97
+ --st-tower-3: #a8bccf;
98
+ --st-pane: #e9f1f8;
99
+ --st-floor: #e2d5c3;
100
+ --st-floor-edge: rgba(60, 40, 20, .08);
101
+ --st-glow: rgba(255, 222, 160, .55);
102
+ --st-glow-out: rgba(255, 222, 160, 0);
103
+ --st-stem: #c4b4a0;
104
+ --st-shade: #d5c5af;
105
+ --st-mug: #cbbba8;
106
+ --st-leaf-1: #6f9a6a;
107
+ --st-leaf-2: #5d8a59;
108
+ --st-ink: #1b1a1d;
109
+ --st-ink-sub: #3f3e44;
110
+ --st-ink-soft: #4d4c52;
111
+ --st-ink-faint: #5a595f;
112
+ --st-ink-dim: #66656b;
113
+ --st-ink-hint: #66656b;
114
+ --st-glass: rgba(255, 255, 255, .52);
115
+ --st-glass-edge: rgba(60, 40, 20, .16);
116
+ --st-rule: rgba(60, 40, 20, .14);
117
+ --st-link: #96410f;
118
+ --st-credits: #96410f;
119
+ --st-focus: #96410f;
120
+ --st-resume: rgba(251, 149, 105, .22);
121
+ --st-resume-edge: rgba(190, 85, 35, .6);
122
+ --st-word-hover: rgba(60, 40, 20, .28);
123
+ --st-word-ring: rgba(190, 85, 35, .6);
124
+ --st-panel: rgba(255, 255, 255, .75);
125
+ --st-panel-edge: rgba(60, 40, 20, .16);
126
+ --st-panel-rim: rgba(255, 255, 255, .9);
127
+ --st-panel-drop: rgba(60, 40, 20, .18);
128
+ --st-close: rgba(60, 40, 20, .08);
129
+ --st-chip-edge: rgba(60, 40, 20, .2);
130
+ --st-track: rgba(60, 40, 20, .12);
131
+ --st-danger: #bb1f1f;
132
+ }
133
+ /* The panels' scrim is the day wall's own tint, whatever the hall's theme, set
134
+ on the overlay so it outranks the theme's .42 and the phone's .88. */
135
+ body[data-page="studio"][data-sky="day"] .st-overlay {
136
+ --st-scrim: rgba(239, 231, 220, .9);
137
+ color-scheme: light;
138
+ }
139
+ body[data-page="studio"][data-sky="day"] .studio { color-scheme: light; }
140
+ /* The Approval queue (approval-queue.css) is night glass on its own root; over
141
+ the day room it wears the same day glass as the studio's own panels. Its
142
+ rendered page stays on its paper frame, and its accent button stays the
143
+ accent, which carries black words at AA in either sky. */
144
+ body[data-page="studio"][data-sky="day"] .aq-overlay {
145
+ color-scheme: light;
146
+ --aq-scrim: rgba(239, 231, 220, .9);
147
+ --aq-glass: rgba(255, 255, 255, .75);
148
+ --aq-glass-edge: rgba(60, 40, 20, .16);
149
+ --aq-glass-rim: rgba(255, 255, 255, .9);
150
+ --aq-drop: rgba(60, 40, 20, .18);
151
+ --aq-ink: #1b1a1d;
152
+ --aq-ink-soft: #4d4c52;
153
+ --aq-ink-faint: #5a595f;
154
+ --aq-ink-dim: #66656b;
155
+ --aq-well: rgba(60, 40, 20, .07);
156
+ --aq-pill-on: rgba(255, 255, 255, .9);
157
+ --aq-close: rgba(60, 40, 20, .08);
158
+ --aq-ghost-edge: rgba(60, 40, 20, .3);
159
+ --aq-link: #96410f;
160
+ --aq-rule: rgba(60, 40, 20, .12);
161
+ }
162
+ body[data-page="studio"][data-sky="day"] .aq-overlay :focus-visible { outline-color: var(--st-focus); }
163
+
72
164
  /* The stage fills the content box: the hall's content column is a reading
73
165
  width, and the artboard is the whole window. */
74
166
  body[data-page="studio"] .app-content { max-width: none; padding: 0; }
@@ -87,7 +179,7 @@ body[data-page="studio"] .app-foot { display: none; }
87
179
  }
88
180
  html[data-fullscreen="on"] .studio { min-height: 100vh; }
89
181
  .studio a { color: var(--st-link); }
90
- .studio a:hover { color: var(--st-accent); }
182
+ .studio a:hover { color: var(--st-focus); }
91
183
  .studio button { font-family: inherit; }
92
184
 
93
185
  /* In full screen the shell's one button floats over the night room in either
@@ -140,13 +232,23 @@ html[data-fullscreen="on"] body[data-page="studio"] .fullscreen-toggle {
140
232
  .st-star--3 { left: 260px; top: 120px; width: 3px; height: 3px; opacity: .7; }
141
233
  .st-star--4 { left: 470px; top: 170px; width: 2px; height: 2px; }
142
234
  .st-star--5 { left: 130px; top: 190px; width: 2px; height: 2px; opacity: .6; }
235
+ /* The clouds belong to the day room; at night they are not there. */
236
+ .st-cloud { border-radius: 40px; background: var(--st-star); opacity: 0; }
237
+ .st-cloud::before,
238
+ .st-cloud::after { content: ""; position: absolute; background: inherit; border-radius: 50%; }
239
+ .st-cloud--1 { left: 44px; top: 76px; width: 118px; height: 28px; }
240
+ .st-cloud--1::before { left: 24px; top: -17px; width: 47px; height: 41px; }
241
+ .st-cloud--1::after { left: 58px; top: -11px; width: 37px; height: 32px; }
242
+ .st-cloud--2 { left: 248px; top: 150px; width: 86px; height: 22px; }
243
+ .st-cloud--2::before { left: 17px; top: -13px; width: 34px; height: 30px; }
244
+ .st-cloud--2::after { left: 43px; top: -9px; width: 28px; height: 24px; }
143
245
  .st-tower { bottom: 0; }
144
246
  .st-tower--1 { left: 0; width: 120px; height: 230px; background: var(--st-tower-1); }
145
247
  .st-tower--2 { left: 110px; width: 90px; height: 310px; background: var(--st-tower-2); }
146
248
  .st-tower--3 { left: 200px; width: 140px; height: 200px; background: var(--st-tower-3); }
147
249
  .st-tower--4 { left: 330px; width: 80px; height: 270px; background: var(--st-tower-2); }
148
250
  .st-tower--5 { left: 400px; width: 140px; height: 180px; background: var(--st-tower-1); }
149
- .st-light { width: 6px; height: 8px; background: var(--st-accent); }
251
+ .st-light { width: 6px; height: 8px; background: var(--st-pane); }
150
252
  .st-light--1 { left: 136px; bottom: 250px; opacity: .75; }
151
253
  .st-light--2 { left: 160px; bottom: 210px; opacity: .55; }
152
254
  .st-light--3 { left: 30px; bottom: 150px; opacity: .6; }
@@ -161,6 +263,14 @@ html[data-fullscreen="on"] body[data-page="studio"] .fullscreen-toggle {
161
263
  background: var(--st-floor);
162
264
  border-top: 1px solid var(--st-floor-edge);
163
265
  }
266
+ /* By day the lamp is off and a patch of sun lies on the floor instead. */
267
+ .st-sunpatch {
268
+ left: calc(50% + 140px); top: calc(712px + var(--room-drop)); width: 520px; height: 120px;
269
+ opacity: 0;
270
+ background: var(--st-glow);
271
+ transform: skewX(-28deg);
272
+ filter: blur(10px);
273
+ }
164
274
  .st-glow {
165
275
  left: calc(50% + 440px); top: calc(470px + var(--room-drop)); width: 300px; height: 300px;
166
276
  border-radius: 50%;
@@ -173,6 +283,26 @@ html[data-fullscreen="on"] body[data-page="studio"] .fullscreen-toggle {
173
283
  .st-leaf--1 { left: calc(50% + 180px); top: calc(610px + var(--room-drop)); width: 64px; height: 90px; background: var(--st-leaf-1); }
174
284
  .st-leaf--2 { left: calc(50% + 220px); top: calc(590px + var(--room-drop)); width: 50px; height: 110px; background: var(--st-leaf-2); }
175
285
 
286
+ /* The day room: the sun rides higher and larger than the moon, the stars and
287
+ the lamp's glow are gone, the clouds and the patch of sun are there. */
288
+ body[data-sky="day"] .st-moon { left: 394px; top: 42px; width: 76px; height: 76px; box-shadow: 0 0 80px 26px var(--st-moon-halo); }
289
+ body[data-sky="day"] .st-star,
290
+ body[data-sky="day"] .st-glow { opacity: 0; }
291
+ body[data-sky="day"] .st-cloud { opacity: .9; }
292
+ body[data-sky="day"] .st-sunpatch { opacity: .7; }
293
+
294
+ /* The room's only motion: the clouds drift, and a change of sky fades over
295
+ .8s. Both live only without a reduced-motion preference, and style.css's
296
+ kill switch stops them besides. */
297
+ @media (prefers-reduced-motion: no-preference) {
298
+ .st-cloud { animation: st-drift 60s linear infinite; }
299
+ .st-cloud--2 { animation-delay: -25s; }
300
+ .studio,
301
+ .st-room > *,
302
+ .st-window > * { transition: background-color .8s ease, opacity .8s ease, box-shadow .8s ease; }
303
+ @keyframes st-drift { from { transform: translateX(-30px); } to { transform: translateX(60px); } }
304
+ }
305
+
176
306
  /* Everything the artist reads or presses sits above the room. */
177
307
  .st-top,
178
308
  .st-words,
@@ -206,7 +336,7 @@ html[data-fullscreen="on"] body[data-page="studio"] .fullscreen-toggle {
206
336
  .st-tally[hidden] { display: none; }
207
337
  .st-tally__item { display: flex; align-items: baseline; gap: 6px; white-space: nowrap; }
208
338
  .st-tally__n { font-size: 20px; font-weight: 300; color: var(--st-ink); font-variant-numeric: tabular-nums; }
209
- .st-tally__n--credits { color: var(--st-accent); }
339
+ .st-tally__n--credits { color: var(--st-credits); }
210
340
  .st-tally__label { font-size: 13px; color: var(--st-ink-soft); }
211
341
  .st-tally__rule { width: 1px; height: 20px; background: var(--st-rule); }
212
342
 
@@ -239,7 +369,7 @@ html[data-fullscreen="on"] body[data-page="studio"] .fullscreen-toggle {
239
369
  }
240
370
  .st-words [data-words] { cursor: text; border-radius: 2px; }
241
371
  .st-words [data-words]:hover { box-shadow: 0 1px 0 var(--st-word-hover); }
242
- .st-words [data-words][contenteditable="true"]:focus { box-shadow: 0 1px 0 var(--st-accent); }
372
+ .st-words [data-words][contenteditable="true"]:focus { box-shadow: 0 1px 0 var(--st-focus); }
243
373
  .st-words [data-words][contenteditable="true"]:focus-visible {
244
374
  outline: 1px solid var(--st-word-ring);
245
375
  outline-offset: 6px;
@@ -356,7 +486,7 @@ html[data-fullscreen="on"] body[data-page="studio"] .fullscreen-toggle {
356
486
  .st-resume:focus-visible,
357
487
  .st-action:focus-visible,
358
488
  .st-hint:focus-visible,
359
- .st-older:focus-visible { outline: 2px solid var(--st-accent); outline-offset: 3px; }
489
+ .st-older:focus-visible { outline: 2px solid var(--st-focus); outline-offset: 3px; }
360
490
 
361
491
  /* The older reviews' door: a quiet line under the actions, not a fourth row. */
362
492
  .st-older {
@@ -421,7 +551,7 @@ html.st-open { overflow: hidden; }
421
551
  gap: 22px;
422
552
  }
423
553
  .st-overlay button { font-family: inherit; }
424
- .st-overlay :focus-visible { outline: 2px solid var(--st-accent); outline-offset: 2px; }
554
+ .st-overlay :focus-visible { outline: 2px solid var(--st-focus); outline-offset: 2px; }
425
555
  .st-dialog__head { display: flex; align-items: center; gap: 12px; }
426
556
  .st-dialog__title {
427
557
  margin: 0;
@@ -510,7 +640,7 @@ html.st-open { overflow: hidden; }
510
640
  color: var(--st-ink); text-decoration: none;
511
641
  }
512
642
  .st-site:hover { background: var(--st-glass); }
513
- .st-site:focus-visible { outline: 2px solid var(--st-accent); outline-offset: 1px; }
643
+ .st-site:focus-visible { outline: 2px solid var(--st-focus); outline-offset: 1px; }
514
644
  .st-site__head { display: flex; align-items: baseline; gap: 4px 12px; flex-wrap: wrap; }
515
645
  .st-site__page { font-size: 17px; font-weight: 300; }
516
646
  .st-site__meta { font-size: 13px; color: var(--st-ink-dim); }
@@ -551,6 +681,7 @@ html.st-open { overflow: hidden; }
551
681
  .st-actions { margin-top: 36px; width: 100%; }
552
682
  .st-floor { top: auto; bottom: 0; min-height: 0; height: 96px; }
553
683
  .st-glow { left: auto; right: -110px; top: auto; bottom: -20px; }
684
+ .st-sunpatch { left: auto; right: 40px; top: auto; bottom: 10px; width: 300px; height: 70px; }
554
685
  .st-lamp-stem { left: auto; right: 44px; top: auto; bottom: 96px; height: 100px; }
555
686
  .st-lamp-shade { left: auto; right: 0; top: auto; bottom: 170px; }
556
687
  .st-mug { left: auto; right: 116px; top: auto; bottom: 96px; }
@@ -13,7 +13,7 @@
13
13
  <link rel="preconnect" href="https://fonts.googleapis.com">
14
14
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
15
15
  <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
16
- <link rel="stylesheet" href="/builders/studio.css?v=2026-09-28-tw16">
16
+ <link rel="stylesheet" href="/builders/studio.css?v=2026-10-09-day">
17
17
  <link rel="stylesheet" href="/builders/approval-queue.css?v=2026-09-29-aqfix">
18
18
  </head>
19
19
  <!-- THE STUDIO HOME (task 1003971 / BV2.TW16, goal 1000095; ADR 0341 D2, D5,
@@ -40,7 +40,10 @@
40
40
  <!-- data-apex: the instance's own address, stamped by the server from the
41
41
  branding pack (the landing surface's line in Artist Review Status). -->
42
42
  <main id="hall-main" class="studio" data-apex="{{publicOrigin}}">
43
- <!-- The night room. Drawn, not an image: it is the artboard's own shapes. -->
43
+ <!-- The room. Drawn, not an image: it is the artboard's own shapes. By the
44
+ viewer's clock it is the night room or the day room (task 1004654,
45
+ docs/design/mocks/day-night-rooms): studio.js sets data-sky on the
46
+ body, and the clouds and the patch of sun are only seen by day. -->
44
47
  <div class="st-room" aria-hidden="true">
45
48
  <div class="st-window">
46
49
  <span class="st-moon"></span>
@@ -49,6 +52,8 @@
49
52
  <span class="st-star st-star--3"></span>
50
53
  <span class="st-star st-star--4"></span>
51
54
  <span class="st-star st-star--5"></span>
55
+ <span class="st-cloud st-cloud--1"></span>
56
+ <span class="st-cloud st-cloud--2"></span>
52
57
  <span class="st-tower st-tower--1"></span>
53
58
  <span class="st-tower st-tower--2"></span>
54
59
  <span class="st-tower st-tower--3"></span>
@@ -64,6 +69,7 @@
64
69
  <span class="st-mullion"></span>
65
70
  </div>
66
71
  <div class="st-floor"></div>
72
+ <div class="st-sunpatch"></div>
67
73
  <div class="st-glow"></div>
68
74
  <div class="st-lamp-stem"></div>
69
75
  <div class="st-lamp-shade"></div>
@@ -199,7 +205,7 @@
199
205
  <!-- The page names and the draft body (the tweak editor's pure rules), then
200
206
  the studio's own. -->
201
207
  <script src="/builders/tweak-editor-lib.js?v=2026-09-28-tw23"></script>
202
- <script src="/builders/studio-lib.js?v=2026-09-28-tw16"></script>
203
- <script src="/builders/studio.js?v=2026-09-28-tw16"></script>
208
+ <script src="/builders/studio-lib.js?v=2026-10-09-day"></script>
209
+ <script src="/builders/studio.js?v=2026-10-09-day"></script>
204
210
  </body>
205
211
  </html>
@@ -838,7 +838,10 @@
838
838
  wedit.lastSent = JSON.stringify(editorLib.draftBody(wedit.readingHash, wedit.lines, wedit.values));
839
839
  wedit.keys = new Map();
840
840
  for (const el of wordEls()) {
841
- const line = lib.matchLine(el.textContent, wedit.lines);
841
+ // The greeting is matched by the words the page was served with first:
842
+ // the clock may be showing "Good morning." over the page's one greeting
843
+ // line. A reading taken in daylight may list only the shown words.
844
+ const line = (el === greetEl && lib.matchLine(servedGreeting, wedit.lines)) || lib.matchLine(el.textContent, wedit.lines);
842
845
  if (!line) { el.setAttribute('data-unread', ''); continue; }
843
846
  wedit.keys.set(el, line.key);
844
847
  el.textContent = wedit.values[line.key];
@@ -929,6 +932,40 @@
929
932
  if (wedit.again) { wedit.again = false; saveWords(); }
930
933
  }
931
934
 
935
+ // ---- the room's sky and the greeting (task 1004654) ------------------------
936
+ // The day room from 07:00 to 18:59 on the viewer's own clock and the night
937
+ // room otherwise (docs/design/mocks/day-night-rooms), re-checked each minute.
938
+ // Only an attribute changes here: studio.css draws both rooms, and the kill
939
+ // switch stops their motion.
940
+ const SKY_RECHECK_MS = 60000;
941
+ const greetEl = byId('studio-greet');
942
+ const servedGreeting = greetEl ? greetEl.textContent : '';
943
+
944
+ // The greeting follows the time of day while the page still carries the
945
+ // stock one, and never while the artist is typing over it: an artist's tweak
946
+ // of the studio page always wins. Written here, not in studio-lib.js, so the
947
+ // copy registry places each greeting at its own assignment.
948
+ function paintGreeting(part) {
949
+ if (!greetEl || wedit.editing || !lib.clockGreets(servedGreeting)) return;
950
+ if (part === 'morning') greetEl.textContent = 'Good morning.';
951
+ else if (part === 'afternoon') greetEl.textContent = 'Good afternoon.';
952
+ else greetEl.textContent = servedGreeting;
953
+ }
954
+
955
+ function paintSky() {
956
+ const now = new Date();
957
+ if (document.body) document.body.setAttribute('data-sky', lib.skyAt(now));
958
+ paintGreeting(lib.dayPart(now));
959
+ }
960
+
961
+ // Synchronous, before the first read, so the room is drawn in the right sky
962
+ // from the start rather than turning to day after the network answers.
963
+ function startSky() {
964
+ if (!hasDom || !lib) return;
965
+ paintSky();
966
+ if (typeof setInterval === 'function') setInterval(paintSky, SKY_RECHECK_MS);
967
+ }
968
+
932
969
  function wireHome() {
933
970
  const flow = byId('st-flow');
934
971
  if (flow) flow.addEventListener('click', () => openPanel('flow', flow));
@@ -1001,6 +1038,7 @@
1001
1038
  }
1002
1039
 
1003
1040
  (async () => {
1041
+ startSky();
1004
1042
  mountApprovals();
1005
1043
  const me = await api.request('GET', `${API}/me`);
1006
1044
  if (me.ok) viewer = me.data.builder || me.data;
@@ -23,6 +23,22 @@ The page keeps its words until the round is submitted, applied, approved and shi
23
23
 
24
24
  **Why the sub-line's sentence is in studio.js, not studio-lib.js.** The copy registry places a line at its `textContent` assignment. Built inside the lib it read as unplaced, and the reader's pattern tier then matched it to the generic `{…} of {…}` row (sky-panel.js). Written where studio.js assigns it, it is placed on studio.js, which is the page's own file and wins.
25
25
 
26
+ ## The day room (task 1004654)
27
+
28
+ From 07:00 to 18:59 on the viewer's own clock the room is the day room of the owner-approved mockup [`docs/design/mocks/day-night-rooms`](../../../docs/design/mocks/day-night-rooms/README.md) ("Studio", sky "Day"), and the night room otherwise. studio.js sets `data-sky` on the body when the page loads, before the first read, and again every minute. The rule is `skyAt` in studio-lib.js. It is the creation page's rule (projects.html), written again here because hall-ui does not import the landing's code. The day palette is the mockup's `[data-sky="day"]` block, declared on the same `--st-*` names just below the night tokens. The drawing is the same, with clouds and a sun in the window, lighter towers with unlit panes, a warm wall and floor, the lamp off and a patch of sun on the floor. The clouds drift and a change of sky fades, both only without a reduced-motion preference (and style.css's kill switch stops them besides). The hall chrome keeps its own light and dark.
29
+
30
+ The greeting follows the time of day: "Good morning." before noon by day, "Good afternoon." after it, and the page's own "Good evening." at night. It does so only while the served greeting is still the stock "Good evening." (`clockGreets`). Once an artist's tweak of the studio page has rewritten it, their words show at every hour. The clock never types over the words while the artist is editing them.
31
+
32
+ Where it differs from the mockup, and why:
33
+
34
+ - **The link, the credits figure and the focus ring are #96410f by day**, not the mockup's #a3481a link and #fb9569 accent: #a3481a on Resume's tint over the wall is 4.3:1, under AA, and the accent is 1.8:1 on the wall.
35
+ - **The quiet inks are #5a595f (faint) and #66656b (dim, hint)** where the mockup has the one #66656b: the studio has three quiet tiers, and each holds AA on the glass and the bare wall (the hint is the lowest, 4.7:1).
36
+ - **The sun, clouds and sun patch are placed for the studio's 560px window** (the mockup's is 520px): the sun at 394/42 and 76px, the clouds about 8% larger, the patch 20px right to sit under the studio's lamp.
37
+ - **The panels follow the sky**: by day the two glass panels and the Approval queue wear white glass (.75) over a scrim of the wall's own tint (.9) in either hall theme and on a phone. The queue's rendered page stays on its paper frame. The mockup draws no panel.
38
+ - **"Good morning." while being typed over shows the page's greeting line**, "Good evening.", the one line a tweak can rewrite. Rewritten, it is the greeting at every hour.
39
+ - **On a phone the sun patch lies on the phone's floor** (bottom right, 300 by 70); the mockup hides it under 900px, but the studio's phone keeps its desk.
40
+ - **The stars do not twinkle and the lamp does not breathe at night**: the night room stays as it shipped. This task adds only the day room's drift.
41
+
26
42
  ## The older reviews stay one quiet door away
27
43
 
28
44
  The garden this page used to be (R06 of goal 1000074; retired in TW14) still answers the reviews filed before Tweak Mode, because they may hold a deploy under the strict artist gate. It sits behind a quiet line under the actions ("1 review from before Tweak Mode still waits"), shown only while a review waits, and opens in its own dialog in the hall's tokens with the focus card, the stones and the two links, unchanged. Its verdict writes are unchanged.
@@ -1253,6 +1253,9 @@ async function abandonTask({ taskId, reason, authorize = null }, deps = {}) {
1253
1253
  //
1254
1254
  // Restores to **backlog**, not ready: the task was retired deliberately, so it
1255
1255
  // comes back un-workable and needs a second, explicit promote to be claimable.
1256
+ // It comes back PARKED (parked_at/parked_by, core_274, task 1004592): without
1257
+ // the stamp the promotion sweep promoted it on the next tick, so the "second,
1258
+ // explicit promote" never had to happen. `parkedBy` is the restoring builder.
1256
1259
  // abandonTask stamps `value_summary = 'Abandoned: <reason>'`; a restored task
1257
1260
  // clears exactly that stamp (and nothing else) so a live task never carries a
1258
1261
  // retire note into its ship card / broadcast.
@@ -1264,7 +1267,7 @@ async function abandonTask({ taskId, reason, authorize = null }, deps = {}) {
1264
1267
  //
1265
1268
  // #538 test seam: a caller-supplied pool is honored ONLY under NODE_ENV==='test'
1266
1269
  // (matches abandonTask/confirmTask). Production callers omit deps → module pool.
1267
- async function restoreTask({ taskId }, deps = {}) {
1270
+ async function restoreTask({ taskId, parkedBy = null }, deps = {}) {
1268
1271
  const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
1269
1272
  const client = await activePool.connect();
1270
1273
  try {
@@ -1296,13 +1299,15 @@ async function restoreTask({ taskId }, deps = {}) {
1296
1299
  const { rows: updated } = await client.query(
1297
1300
  `UPDATE tasks
1298
1301
  SET status = 'backlog',
1302
+ parked_at = now(),
1303
+ parked_by = $2,
1299
1304
  merged_into_task_id = NULL,
1300
1305
  triage_note = NULL,
1301
1306
  value_summary = CASE WHEN value_summary LIKE 'Abandoned: %' THEN NULL ELSE value_summary END,
1302
1307
  updated_at = now()
1303
1308
  WHERE id = $1
1304
1309
  RETURNING id, title, status`,
1305
- [taskId]
1310
+ [taskId, parkedBy]
1306
1311
  );
1307
1312
 
1308
1313
  await client.query('COMMIT');
@@ -1257,6 +1257,21 @@ async function updateTaskStatus(id, status) {
1257
1257
  await pool.query(`UPDATE tasks SET status = $2 WHERE id = $1`, [id, status]);
1258
1258
  }
1259
1259
 
1260
+ // POST /tasks/:id/demote (task 1004592): ready -> backlog, PARKED. The stamp is
1261
+ // what tells the promotion sweep and the core_274 triggers that a person put
1262
+ // this task here, so neither undoes the demote on the next tick. The status
1263
+ // guard sits in the WHERE so a task that moved since the route read it is left
1264
+ // alone; returns whether the row was demoted.
1265
+ async function parkTask(id, builderId, exec) {
1266
+ const { rowCount } = await (exec || pool).query(
1267
+ `UPDATE tasks
1268
+ SET status = 'backlog', parked_at = now(), parked_by = $2, updated_at = now()
1269
+ WHERE id = $1 AND status = 'ready'`,
1270
+ [id, builderId == null ? null : builderId]
1271
+ );
1272
+ return rowCount > 0;
1273
+ }
1274
+
1260
1275
  // The UNSCOPED half of auto-promotion (task 1003932) — the backlog drain that
1261
1276
  // `runCriterionSweep` already is for criteria.
1262
1277
  //
@@ -1305,9 +1320,18 @@ async function updateTaskStatus(id, status) {
1305
1320
  //
1306
1321
  // The exclusion is by SOURCE, not by discipline: engineer-scoping-on-full-idea
1307
1322
  // is equally a request, and the preamble above is written on both.
1323
+ //
1324
+ // The second kind has a second source: a PERSON parking a task (task 1004592).
1325
+ // restoreTask and POST /tasks/:id/demote both stamp tasks.parked_at, and
1326
+ // `parked_at IS NULL` keeps the sweep off those rows. core_274 puts the same
1327
+ // clause in the three promotion triggers and clears the stamp whenever a task
1328
+ // leaves backlog, so a person's promote is the only way out. Hand-filed backlog
1329
+ // with no parked_at is still drained: create-time intent is not recorded, and
1330
+ // dependencies are often added after create.
1308
1331
  const PROMOTABLE_BACKLOG_WHERE = `
1309
1332
  WHERE t.status = 'backlog'
1310
1333
  AND t.source IS DISTINCT FROM 'cascade'
1334
+ AND t.parked_at IS NULL
1311
1335
  AND task_is_fully_unblocked(t.id)
1312
1336
  AND NOT EXISTS (
1313
1337
  SELECT 1
@@ -1444,6 +1468,7 @@ module.exports = {
1444
1468
  updateTaskRequiresRank,
1445
1469
  updateTaskSecuritySensitive,
1446
1470
  updateTaskStatus,
1471
+ parkTask,
1447
1472
  updateTaskValueSummary,
1448
1473
  versionAcceptsNewWork,
1449
1474
  };