@bongos/core 1.19.1062 → 1.19.1063
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 +71 -31
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +2 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +44 -1
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +44 -7
- package/docs/copy-registry.json +393 -26
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +36 -4
- package/docs/page-readings.json +34 -1
- package/modules/copy-desk/page-status.js +63 -0
- package/modules/copy-desk/pages.js +1 -0
- package/modules/copy-desk/routes/copy-desk.js +30 -0
- package/modules/hall-ui/public/tweak-editor-lib.js +137 -0
- package/modules/hall-ui/public/tweak-editor.css +437 -0
- package/modules/hall-ui/public/tweak-editor.html +111 -0
- package/modules/hall-ui/public/tweak-editor.js +447 -0
- package/modules/hall-ui/public/tweak-editor.states.json +99 -0
- package/modules/hall-ui/records/tweak-editor.md +32 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/hall-preview/server.js +21 -0
- package/src/bongos/serve-internal.js +5 -1
- package/src/module-api.js +1 -1
- package/tests/copy_desk_page_editor.mjs +181 -0
- package/tests/hall_audit.mjs +5 -0
- package/tests/hall_page_gate_map.mjs +3 -0
- package/tests/hall_tweak_editor.mjs +432 -0
|
@@ -1205,8 +1205,12 @@ function mountInternalSurfaces(app) {
|
|
|
1205
1205
|
// GET /sky/mine, with the room inside it as the list drawer. The old ambient
|
|
1206
1206
|
// page at `sky` (task 1004105) is gone; its URL 301s to /thinking below, so it
|
|
1207
1207
|
// leaves this regex — one URL cannot be two tiers, and a redirect is not a page.
|
|
1208
|
+
// task 1004321 (BV2.TW10): `tweak-editor`, Tweak Mode's editor, the artist's
|
|
1209
|
+
// own room beside the studio, on the studio's tier. Its read (GET
|
|
1210
|
+
// /copy-desk/pages/:pageId/draft) is any-builder like the .docx download, and
|
|
1211
|
+
// every write is the page claim's holder only, checked server-side.
|
|
1208
1212
|
const SIGNEDIN_BUILDERS_PAGE_RE =
|
|
1209
|
-
/^\/builders\/(?:work|settings|atlas|modules|goals|goal-map|roadmap|copy-desk|studio|ideas|thinking|collab|task|idea|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
|
|
1213
|
+
/^\/builders\/(?:work|settings|atlas|modules|goals|goal-map|roadmap|copy-desk|studio|tweak-editor|ideas|thinking|collab|task|idea|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
|
|
1210
1214
|
// The diagram image assets under /builders/diagrams/ ride the same tier as the
|
|
1211
1215
|
// page that embeds them.
|
|
1212
1216
|
const SIGNEDIN_BUILDERS_PREFIX_RE = /^\/builders\/diagrams\//;
|
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.1063'; // 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');
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// tests/copy_desk_page_editor.mjs — the read the tweak editor opens on,
|
|
2
|
+
// GET /copy-desk/pages/:pageId/draft, the WHOLE path (task 1004321 / BV2.TW10,
|
|
3
|
+
// ADR 0341 D4).
|
|
4
|
+
//
|
|
5
|
+
// The done-when's middle: an artist rewrites a page, closes the tab, and comes
|
|
6
|
+
// back to the draft. So the read must give back every line of the page in page
|
|
7
|
+
// order with its margin note, say whether the caller holds the round, and hand
|
|
8
|
+
// back exactly the draft the autosave stored, and never a draft saved on
|
|
9
|
+
// another reading (its keys may name other lines).
|
|
10
|
+
//
|
|
11
|
+
// Driven through the real copy-desk routes, the real lifecycle port (the page
|
|
12
|
+
// claim, updateHeldTaskDescription, submitPageTweak) and the real page reads,
|
|
13
|
+
// against the shared stateful fake ledger (tests/fixtures/page-ledger.mjs).
|
|
14
|
+
|
|
15
|
+
import { strict as assert } from 'node:assert';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { makeRunner } from './helpers.mjs';
|
|
21
|
+
import { installPageLedger } from './fixtures/page-ledger.mjs';
|
|
22
|
+
|
|
23
|
+
process.env.NODE_ENV = 'test';
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const F = require('../modules/copy-desk/tests/fixtures/page-tweak.cjs');
|
|
26
|
+
const pageStatus = require('../modules/copy-desk/page-status.js');
|
|
27
|
+
const api = require('../src/module-api.js');
|
|
28
|
+
|
|
29
|
+
installPageLedger(api, {
|
|
30
|
+
builders: [
|
|
31
|
+
{ id: '7', github_login: 'alice', display_name: 'Alice', preferred_disciplines: ['artist'] },
|
|
32
|
+
{ id: '8', github_login: 'bob', display_name: 'Bob', preferred_disciplines: ['artist'] },
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
const ALICE = '7';
|
|
36
|
+
const BOB = '8';
|
|
37
|
+
|
|
38
|
+
api.requireBuilder = (req, res, next) => {
|
|
39
|
+
req.builder = { id: String(req.headers['x-builder'] || ALICE), rank: 'metic' };
|
|
40
|
+
next();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const seams = require('../src/module-seams.js');
|
|
44
|
+
const lifecyclePort = require('../modules/lifecycle/lifecycle.js');
|
|
45
|
+
if (!seams.hasProvider('lifecycle')) seams.registerProvider('lifecycle', lifecyclePort);
|
|
46
|
+
|
|
47
|
+
const pageData = require('../modules/copy-desk/page-data.js');
|
|
48
|
+
const express = require('express');
|
|
49
|
+
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
50
|
+
const buildRouter = require('../modules/copy-desk/routes/copy-desk.js');
|
|
51
|
+
|
|
52
|
+
const app = express();
|
|
53
|
+
app.use(express.json());
|
|
54
|
+
app.use(attachFail);
|
|
55
|
+
app.use(buildRouter());
|
|
56
|
+
const server = await new Promise((resolve) => { const s = app.listen(0, '127.0.0.1', () => resolve(s)); });
|
|
57
|
+
const base = `http://127.0.0.1:${server.address().port}`;
|
|
58
|
+
|
|
59
|
+
async function call(method, p, builder, body = {}) {
|
|
60
|
+
const r = await fetch(base + p, { method, headers: { 'content-type': 'application/json', 'x-builder': builder }, body: method === 'GET' ? undefined : JSON.stringify(body) });
|
|
61
|
+
return { status: r.status, body: await r.json() };
|
|
62
|
+
}
|
|
63
|
+
const errCode = (b) => (b && b.error && b.error.code) || null;
|
|
64
|
+
|
|
65
|
+
const L = (key, section, text, placement, row) => ({ key, section, text, placement, ...(row || {}) });
|
|
66
|
+
const STUDIO_LINES = [
|
|
67
|
+
L('L0001', 'top bar', 'Studio', 'shared', { file: 'modules/hall-ui/public/shell.js', line: 40, string_id: 'sh0001' }),
|
|
68
|
+
L('L0002', 'top bar', 'Ctrl K', 'shared'),
|
|
69
|
+
L('L0003', 'greeting', 'Welcome back', 'placed', { file: 'modules/hall-ui/public/studio.html', line: 12, string_id: 'st0003' }),
|
|
70
|
+
L('L0004', 'greeting', 'Your desk is quiet today', 'unplaced'),
|
|
71
|
+
L('L0005', 'footer', 'Resume where you left off', 'placed', { file: 'modules/hall-ui/public/studio.html', line: 90, string_id: 'st0005' }),
|
|
72
|
+
];
|
|
73
|
+
const readingsWith = (studioHash, lines = STUDIO_LINES) => ({
|
|
74
|
+
schema: 1,
|
|
75
|
+
pages: [{ id: 'builders:studio', reading_hash: studioHash, lines }],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'page-editor-'));
|
|
79
|
+
fs.mkdirSync(path.join(tmp, 'docs'), { recursive: true });
|
|
80
|
+
fs.writeFileSync(path.join(tmp, 'docs/page-inventory.json'), JSON.stringify(F.INVENTORY));
|
|
81
|
+
function writeReadings(r) {
|
|
82
|
+
fs.writeFileSync(path.join(tmp, 'docs/page-readings.json'), JSON.stringify(r));
|
|
83
|
+
pageData._setRoot(tmp); // drop the (mtime, size) cache
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { test, summary } = makeRunner();
|
|
87
|
+
const STUDIO = '/copy-desk/pages/builders:studio';
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
writeReadings(readingsWith('studio-h1'));
|
|
91
|
+
|
|
92
|
+
await test('before anyone claims: every line in page order, with its margin note, and no round', async () => {
|
|
93
|
+
const r = await call('GET', `${STUDIO}/draft`, ALICE);
|
|
94
|
+
assert.equal(r.status, 200, JSON.stringify(r.body));
|
|
95
|
+
assert.equal(r.body.page.title, 'Studio');
|
|
96
|
+
assert.equal(r.body.page.surface_word, 'builders hall');
|
|
97
|
+
assert.equal(r.body.reading.reading_hash, 'studio-h1');
|
|
98
|
+
assert.deepEqual(r.body.reading.lines.map((l) => [l.key, l.section, l.text, l.note]), [
|
|
99
|
+
['L0001', 'top bar', 'Studio', 'shared'],
|
|
100
|
+
// Shared shell text with no registry row cannot be applied mechanically,
|
|
101
|
+
// so it goes to an engineer at submit, and the note says so.
|
|
102
|
+
['L0002', 'top bar', 'Ctrl K', 'engineer'],
|
|
103
|
+
['L0003', 'greeting', 'Welcome back', null],
|
|
104
|
+
['L0004', 'greeting', 'Your desk is quiet today', 'engineer'],
|
|
105
|
+
['L0005', 'footer', 'Resume where you left off', null],
|
|
106
|
+
]);
|
|
107
|
+
assert.deepEqual(r.body.notes, { shared: 'changes this on every page', engineer: 'goes to an engineer' });
|
|
108
|
+
assert.equal(r.body.round, null);
|
|
109
|
+
assert.equal(r.body.draft, null);
|
|
110
|
+
assert.deepEqual(r.body.counter, { changed: 0, total: 5 });
|
|
111
|
+
// The read never exposes where a line lives in the code.
|
|
112
|
+
assert.equal(r.body.reading.lines.some((l) => 'file' in l || 'string_id' in l), false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await test('rewrite, close the tab, come back: the read hands back the saved draft, held by you', async () => {
|
|
116
|
+
assert.equal((await call('POST', `${STUDIO}/claim`, ALICE)).status, 201);
|
|
117
|
+
const saved = await call('PUT', `${STUDIO}/draft`, ALICE, {
|
|
118
|
+
reading_hash: 'studio-h1',
|
|
119
|
+
lines: [{ key: 'L0003', after: 'Welcome home' }, { key: 'L0001', after: 'The studio' }],
|
|
120
|
+
});
|
|
121
|
+
assert.equal(saved.body.outcome, 'saved');
|
|
122
|
+
const back = await call('GET', `${STUDIO}/draft`, ALICE);
|
|
123
|
+
assert.equal(back.status, 200);
|
|
124
|
+
assert.equal(back.body.round.state, 'writing');
|
|
125
|
+
assert.equal(back.body.round.held_by_you, true);
|
|
126
|
+
assert.equal(back.body.round.claim.path, '/copy-desk/pages/builders:studio/claim');
|
|
127
|
+
assert.deepEqual(back.body.draft.lines, [{ key: 'L0001', after: 'The studio' }, { key: 'L0003', after: 'Welcome home' }]);
|
|
128
|
+
assert.equal(back.body.draft.saved_at, saved.body.draft.saved_at);
|
|
129
|
+
assert.deepEqual(back.body.counter, { changed: 2, total: 5 });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await test('another artist sees the round held by its writer, not by them', async () => {
|
|
133
|
+
const r = await call('GET', `${STUDIO}/draft`, BOB);
|
|
134
|
+
assert.equal(r.body.round.held_by_you, false);
|
|
135
|
+
assert.deepEqual(r.body.round.holder, { id: ALICE, login: 'alice' });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await test('a draft saved on an older reading is reported, never laid over the new lines', async () => {
|
|
139
|
+
writeReadings(readingsWith('studio-h2'));
|
|
140
|
+
const r = await call('GET', `${STUDIO}/draft`, ALICE);
|
|
141
|
+
assert.equal(r.body.draft, null);
|
|
142
|
+
assert.equal(r.body.draft_on_older_reading, true);
|
|
143
|
+
assert.deepEqual(r.body.counter, { changed: 0, total: 5 });
|
|
144
|
+
writeReadings(readingsWith('studio-h1'));
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await test('after submit the round reads submitted and nobody holds it', async () => {
|
|
148
|
+
assert.equal((await call('POST', `${STUDIO}/submit`, ALICE)).body.outcome, 'submitted');
|
|
149
|
+
const r = await call('GET', `${STUDIO}/draft`, ALICE);
|
|
150
|
+
assert.equal(r.body.round.state, 'submitted');
|
|
151
|
+
assert.equal(r.body.round.held_by_you, false);
|
|
152
|
+
assert.equal(r.body.round.holder, null);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await test('an unknown page is unknown_page, and a page with no reading is page_unread', async () => {
|
|
156
|
+
const r = await call('GET', '/copy-desk/pages/landing:nowhere/draft', ALICE);
|
|
157
|
+
assert.equal(r.status, 404);
|
|
158
|
+
assert.equal(errCode(r.body), 'unknown_page');
|
|
159
|
+
const u = await call('GET', '/copy-desk/pages/landing:index/draft', ALICE);
|
|
160
|
+
assert.equal(u.status, 503);
|
|
161
|
+
assert.equal(errCode(u.body), 'page_unread');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await test('composeEditor keeps only draft lines the reading still has', () => {
|
|
165
|
+
const out = pageStatus.composeEditor({
|
|
166
|
+
page: { id: 'builders:studio', title: 'Studio', surface: 'builders' },
|
|
167
|
+
readingPage: { reading_hash: 'h', lines: [STUDIO_LINES[2]] },
|
|
168
|
+
round: { id: 9, status: 'active', holder_id: 7, holder_login: 'alice', description: '```page-tweak-draft\n' + JSON.stringify({ schema: 1, page_id: 'builders:studio', reading_hash: 'h', saved_at: 'x', lines: [
|
|
169
|
+
{ key: 'L0003', section: 'greeting', before: 'Welcome back', after: 'Hi', placement: 'placed' },
|
|
170
|
+
{ key: 'L0099', section: 'gone', before: 'Old', after: 'New', placement: 'placed' },
|
|
171
|
+
] }) + '\n```\n' },
|
|
172
|
+
builderId: 7,
|
|
173
|
+
});
|
|
174
|
+
assert.deepEqual(out.draft.lines, [{ key: 'L0003', after: 'Hi' }]);
|
|
175
|
+
assert.equal(out.round.held_by_you, true);
|
|
176
|
+
});
|
|
177
|
+
} finally {
|
|
178
|
+
server.close();
|
|
179
|
+
pageData._setRoot(null);
|
|
180
|
+
}
|
|
181
|
+
summary();
|
package/tests/hall_audit.mjs
CHANGED
|
@@ -424,6 +424,11 @@ const LITERAL_EXEMPT = {
|
|
|
424
424
|
'thinking.css': 'the sky\'s own sheet is its token tier (task 1004231, ADR 0331 — the design of record)',
|
|
425
425
|
'sky-interaction.js': 'the sky\'s hover card writes the template\'s ring colour inline (task 1004231)',
|
|
426
426
|
'sky-panel.js': 'the sky\'s panel writes the template\'s dead-bar colour inline (task 1004231)',
|
|
427
|
+
// The tweak editor (task 1004321 / BV2.TW10): docs/design/mocks/tweak-mode/
|
|
428
|
+
// Editor.dc.html is the owner-approved master design, built 1:1: the night
|
|
429
|
+
// studio, the glass bar, and a sheet of PAPER that stays paper in both modes,
|
|
430
|
+
// so its palette cannot be the hall's mode-flipping tier. The sky precedent.
|
|
431
|
+
'tweak-editor.css': 'the tweak editor\'s stage is the artboard\'s own palette (task 1004321, the pixel contract: paper in both modes)',
|
|
427
432
|
};
|
|
428
433
|
const COLOUR_PROPS = 'color|background|background-color|border|border-color|border-top-color|border-bottom-color|fill|stroke|outline|outline-color|box-shadow|text-shadow';
|
|
429
434
|
const colourLiteralRe = () =>
|
|
@@ -62,6 +62,9 @@ const ORACLE = {
|
|
|
62
62
|
// whole gate, and its two reads (GET /tasks, GET /copy-desk/queue) already gate
|
|
63
63
|
// themselves server-side.
|
|
64
64
|
'/builders/studio': 'requireBuilderPage',
|
|
65
|
+
// task 1004321 (BV2.TW10): the tweak editor, on the studio's own tier. Its read
|
|
66
|
+
// is any-builder like the .docx download; every write is the page holder's only.
|
|
67
|
+
'/builders/tweak-editor': 'requireBuilderPage',
|
|
65
68
|
'/builders/ideas': 'requireBuilderPage',
|
|
66
69
|
// THE ROOM (task 1004098 / BV2.R03) — a builder's own idea trail. Sign-in and
|
|
67
70
|
// no rank floor, the tier /ideas sits on: there is no permission gate on
|