@bongos/core 1.21.8 → 1.21.10
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 +59 -34
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +8 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0363-who-decides-what-is-a-table-changed-only-by-amendment.md +52 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +182 -3
- package/docs/api-reference.md +4 -2
- package/docs/copy-inventory.md +8 -27
- package/docs/copy-registry.json +6 -199
- package/docs/module-api-changelog.md +4 -0
- package/modules/discord/board-broadcast.js +2 -0
- package/modules/government/board.js +82 -23
- package/modules/government/db.js +81 -6
- package/modules/government/decision-board.js +300 -0
- package/modules/government/decision-rules.js +312 -0
- package/modules/government/docket.js +49 -9
- package/modules/government/migrations/government_023_decision_rules.sql +190 -0
- package/modules/government/routes/government.js +115 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/copy-inventory.js +24 -7
- package/src/bongos/route-rank-check.js +6 -0
- package/src/module-api.js +1 -1
- package/tests/copy_inventory.mjs +62 -0
- package/tests/government_abuse_matrix.mjs +3 -0
- package/tests/government_decision_rules.mjs +701 -0
- package/tests/government_routes.mjs +4 -0
- package/tests/government_seed.mjs +1 -0
|
@@ -122,6 +122,10 @@ const governmentPort = {
|
|
|
122
122
|
// `charter` block. Opening a sitting decides nothing; the vote does.
|
|
123
123
|
proposeCharter: board.proposeCharter,
|
|
124
124
|
genesisStageStatus: board.genesisStageStatus,
|
|
125
|
+
// task 1004529 — who decides what. The table, resolved, and the check a
|
|
126
|
+
// building's own direct route runs before letting one builder act alone.
|
|
127
|
+
decisionRulesView: board.decisionRulesView,
|
|
128
|
+
checkActAlone: board.checkActAlone,
|
|
125
129
|
};
|
|
126
130
|
|
|
127
131
|
function registerSeams() {
|
|
@@ -225,6 +229,31 @@ module.exports = function buildGovernmentRouter() {
|
|
|
225
229
|
}
|
|
226
230
|
return rank;
|
|
227
231
|
}
|
|
232
|
+
// WHO DECIDES AT PEOPLE (task 1004529). Giving or taking a custom rank is
|
|
233
|
+
// People's own act, and the project's decision-rules table says who may do it
|
|
234
|
+
// alone. As today (the default row) the route's own permission is the whole
|
|
235
|
+
// gate, unchanged. Once a passed amendment hands People to a sitting, one
|
|
236
|
+
// builder may no longer do it alone — the route refuses and names the matter
|
|
237
|
+
// route instead — unless the row's rule lets a seated member decide alone and
|
|
238
|
+
// this builder is seated. Server-side, per request, uncached (ADR 0016).
|
|
239
|
+
// Fail-CLOSED: an unreadable row refuses rather than waving the act through.
|
|
240
|
+
async function peopleMayActAlone(req, res) {
|
|
241
|
+
let out;
|
|
242
|
+
try {
|
|
243
|
+
out = await board.checkActAlone('people', req.builder.id);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
log.error('[gds] people decision rule unreadable', err);
|
|
246
|
+
res.fail('decision_rule_unreadable', { status: 503, message: 'who decides at People could not be read just now' });
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
if (out.ok) return true;
|
|
250
|
+
res.fail('decided_by_sitting', 409, {
|
|
251
|
+
area: out.area, rule: out.rule, who: out.who,
|
|
252
|
+
what_this_means: out.what_this_means, open_with: out.open_with,
|
|
253
|
+
});
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
228
257
|
const ARCHON = 'archon';
|
|
229
258
|
const ARCHON_REASON = 'the Archon rank holds every permission and is immutable — it is the rank that can never be locked out of this tab (ADR 0172)';
|
|
230
259
|
const ASSIGN_REASON = 'a seeded rank is assigned by rank (R95b sync); change the builder\'s rank instead';
|
|
@@ -447,6 +476,7 @@ module.exports = function buildGovernmentRouter() {
|
|
|
447
476
|
try {
|
|
448
477
|
const rank = await loadAssignableRank(res, rankKey);
|
|
449
478
|
if (rank === null) return;
|
|
479
|
+
if (!(await peopleMayActAlone(req, res))) return;
|
|
450
480
|
const assigned = await db.assignRank({
|
|
451
481
|
builderId: req.body.builder_id,
|
|
452
482
|
rankKey,
|
|
@@ -472,6 +502,7 @@ module.exports = function buildGovernmentRouter() {
|
|
|
472
502
|
try {
|
|
473
503
|
const rank = await loadAssignableRank(res, rankKey);
|
|
474
504
|
if (rank === null) return;
|
|
505
|
+
if (!(await peopleMayActAlone(req, res))) return;
|
|
475
506
|
const removed = await db.unassignRank({ builderId, rankKey });
|
|
476
507
|
res.json({ ok: true, builder_id: builderId, rank_key: rankKey, removed });
|
|
477
508
|
} catch (err) {
|
|
@@ -678,6 +709,90 @@ module.exports = function buildGovernmentRouter() {
|
|
|
678
709
|
}
|
|
679
710
|
});
|
|
680
711
|
|
|
712
|
+
// rank: perm board.item.open — propose a change to WHO DECIDES WHAT (task
|
|
713
|
+
// 1004529): one or more buildings' rows of the decision-rules table, filed as
|
|
714
|
+
// an amendment of kind 'decision_rules' and sat under the constitution IN
|
|
715
|
+
// FORCE, like every amendment. Nothing here changes the table — only the
|
|
716
|
+
// sitting passing does, inside its closing transaction. The same atom as
|
|
717
|
+
// proposing any amendment.
|
|
718
|
+
//
|
|
719
|
+
// Body: { rules: [{ area, decided_by, rule?, who? }], rationale_md? }
|
|
720
|
+
// area ∈ rules disputes people planning automation review release treasury
|
|
721
|
+
// (the charter is the board — change it with a constitution amendment)
|
|
722
|
+
// decided_by 'gate' (as today) | 'nobody' (vacant) | rank:<key> | rank:<key>+ | rank:a,b
|
|
723
|
+
// rule decide | first | consent | majority | unanimous | signoff (vacant with 'nobody';
|
|
724
|
+
// none with 'gate')
|
|
725
|
+
// who the seat in words, display only ("Mira under $50, the council above")
|
|
726
|
+
router.post('/government/board/decision-rules', api.requireBuilder, api.requirePermission('board.item.open'), async (req, res) => {
|
|
727
|
+
if (validateOrRespond(req, res, {
|
|
728
|
+
rules: { required: true, type: 'array' },
|
|
729
|
+
rationale_md: { type: 'string', maxLength: 5000 },
|
|
730
|
+
})) return;
|
|
731
|
+
try {
|
|
732
|
+
const out = await board.proposeDecisionRules({
|
|
733
|
+
rules: req.body.rules,
|
|
734
|
+
rationaleMd: req.body.rationale_md ?? null,
|
|
735
|
+
proposedBy: req.builder.id,
|
|
736
|
+
});
|
|
737
|
+
if (!out.ok) {
|
|
738
|
+
if (out.reason === 'no_change') return res.fail('no_change', 409, { reason: 'every building named is already decided this way' });
|
|
739
|
+
return res.fail('invalid_decision_rules', 400, {
|
|
740
|
+
reason: out.reason,
|
|
741
|
+
...(out.area ? { area: out.area } : {}),
|
|
742
|
+
...(out.field ? { field: out.field } : {}),
|
|
743
|
+
...(out.expected ? { expected: out.expected } : {}),
|
|
744
|
+
...(out.rank_key ? { rank_key: out.rank_key } : {}),
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
res.status(201).json({ ok: true, amendment: out.amendment, item: out.item, rules: out.rules });
|
|
748
|
+
} catch (err) {
|
|
749
|
+
log.error('[gds] POST /government/board/decision-rules', err);
|
|
750
|
+
res.fail('propose_failed', { status: 500, message: 'internal error' });
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
// rank: perm board.item.open — put a MATTER to a building's sitting (task
|
|
755
|
+
// 1004529): the board's new subjects, one per building other than the
|
|
756
|
+
// charter. Refused unless the building's row names a sitting — under the
|
|
757
|
+
// default rows every building is decided as today and this route opens
|
|
758
|
+
// nothing. The sitting is snapshotted from the row (who and how).
|
|
759
|
+
//
|
|
760
|
+
// Body: { area, title, detail_md?, action? }
|
|
761
|
+
// action (People only): { type: 'assign_rank' | 'unassign_rank', builder_id, rank_key }
|
|
762
|
+
// — carried out when the sitting passes, under the same walls as the direct route.
|
|
763
|
+
router.post('/government/board/matters', api.requireBuilder, api.requirePermission('board.item.open'), async (req, res) => {
|
|
764
|
+
if (validateOrRespond(req, res, {
|
|
765
|
+
area: { required: true, type: 'string', maxLength: 32 },
|
|
766
|
+
title: { required: true, type: 'string', maxLength: 200 },
|
|
767
|
+
detail_md: { type: 'string', maxLength: 5000 },
|
|
768
|
+
action: { type: 'object' },
|
|
769
|
+
})) return;
|
|
770
|
+
try {
|
|
771
|
+
const out = await board.openMatter({
|
|
772
|
+
area: req.body.area,
|
|
773
|
+
title: req.body.title,
|
|
774
|
+
detailMd: req.body.detail_md ?? null,
|
|
775
|
+
action: req.body.action ?? null,
|
|
776
|
+
proposedBy: req.builder.id,
|
|
777
|
+
});
|
|
778
|
+
if (!out.ok) {
|
|
779
|
+
if (out.reason === 'decided_by_gate' || out.reason === 'no_one_holds_this') {
|
|
780
|
+
return res.fail(out.reason, 409, { area: out.area, ...(out.gate ? { gate: out.gate } : {}), what_this_means: out.what_this_means });
|
|
781
|
+
}
|
|
782
|
+
return res.fail('invalid_matter', 400, {
|
|
783
|
+
reason: out.reason,
|
|
784
|
+
...(out.field ? { field: out.field } : {}),
|
|
785
|
+
...(out.expected ? { expected: out.expected } : {}),
|
|
786
|
+
...(out.rank_key ? { rank_key: out.rank_key } : {}),
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
res.status(201).json({ ok: true, matter: out.matter, item: out.item, decided: out.decided });
|
|
790
|
+
} catch (err) {
|
|
791
|
+
log.error('[gds] POST /government/board/matters', err);
|
|
792
|
+
res.fail('matter_failed', { status: 500, message: 'internal error' });
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
|
|
681
796
|
// rank: perm board.vote.cast — cast a vote on an open board item (R08 of goal
|
|
682
797
|
// 1000069, ADR 0175). The atom FLOORS AT XENOS deliberately (the R04 carry-
|
|
683
798
|
// note): the permission is only the coarse gate, and the real wall is the
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.10",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.10",
|
|
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.21.
|
|
3
|
+
"version": "1.21.10",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -8804,5 +8804,17 @@
|
|
|
8804
8804
|
"id": "1004441",
|
|
8805
8805
|
"text": "Checked and signed off on the live site: artists and ideators now read as their own craft on profiles, the builders list and the status page. Their earnings charts show, and builders can choose their main role in Settings."
|
|
8806
8806
|
}
|
|
8807
|
+
],
|
|
8808
|
+
"1.21.9": [
|
|
8809
|
+
{
|
|
8810
|
+
"id": "1004529",
|
|
8811
|
+
"text": "Each project can now set who decides what in each part of its government: the charter, rules, disputes, people, planning, automation, review, release and treasury. For each part it names who decides and how: one person decides"
|
|
8812
|
+
}
|
|
8813
|
+
],
|
|
8814
|
+
"1.21.10": [
|
|
8815
|
+
{
|
|
8816
|
+
"id": "1004545",
|
|
8817
|
+
"text": "The list of every piece of on-screen text now comes out the same on a Windows computer as on the build server, so updating it on Windows no longer breaks the automatic checks. Notes the authors left inside page source are no l"
|
|
8818
|
+
}
|
|
8807
8819
|
]
|
|
8808
8820
|
}
|
|
@@ -119,8 +119,12 @@ function lineAt(starts, index) {
|
|
|
119
119
|
|
|
120
120
|
// Does this look like prose a person reads, rather than code that happens to be a
|
|
121
121
|
// string? Conservative on purpose — a false positive costs an artist's attention.
|
|
122
|
+
//
|
|
123
|
+
// Measured with CRLF folded to LF: the length cap must give a multi-line run the
|
|
124
|
+
// same verdict on a Windows checkout as on CI's, or the committed registry differs
|
|
125
|
+
// by platform (task 1004545 — a 396-char run was 403 under CRLF and vanished).
|
|
122
126
|
function looksLikeCopy(raw) {
|
|
123
|
-
const s = String(raw).trim();
|
|
127
|
+
const s = String(raw).replace(/\r\n/g, '\n').trim();
|
|
124
128
|
if (s.length < 4 || s.length > 400) return false;
|
|
125
129
|
if (!/[A-Za-z]{2}/.test(s)) return false; // needs actual letters
|
|
126
130
|
if (!/\s/.test(s) && !/[.!?]$/.test(s)) return false; // one bare word is a token
|
|
@@ -155,12 +159,25 @@ function trimmedSpan(group, groupStart) {
|
|
|
155
159
|
return { start: groupStart + lead, end: groupStart + group.length - trail };
|
|
156
160
|
}
|
|
157
161
|
|
|
158
|
-
// Strip <script>/<style> bodies so their contents are not read
|
|
159
|
-
// Replaced with spaces of equal length so every line number
|
|
162
|
+
// Strip HTML comments and <script>/<style> bodies so their contents are not read
|
|
163
|
+
// as page text. Replaced with spaces of equal length so every line number — and
|
|
164
|
+
// every span copy-apply.js writes through — stays truthful.
|
|
165
|
+
//
|
|
166
|
+
// Comments are author notes, never something a visitor reads (task 1004545: a
|
|
167
|
+
// header comment's literal `<a>` opened a fake text node). ONE alternation, not
|
|
168
|
+
// three passes, so whichever construct opens first wins, as in a browser: a
|
|
169
|
+
// `<style>` named inside a comment cannot swallow the page up to the real
|
|
170
|
+
// `</style>`, and a `<!--` inside a script string cannot eat past `</script>`.
|
|
171
|
+
//
|
|
172
|
+
// A comment keeps its `<` and `>` and blanks only its inside: marker comments
|
|
173
|
+
// (`<!--apex-only:start-->`, `<!--brand-if:…-->`) split a sentence into segments
|
|
174
|
+
// the server shows or drops independently, so each segment must stay its own row
|
|
175
|
+
// with its own stable id — blanking the whole comment would fuse them.
|
|
160
176
|
function stripEmbedded(html) {
|
|
161
|
-
return html
|
|
162
|
-
|
|
163
|
-
.
|
|
177
|
+
return html.replace(/<!--[\s\S]*?-->|<script\b[\s\S]*?<\/script>|<style\b[\s\S]*?<\/style>/gi, (m) => {
|
|
178
|
+
const blank = m.replace(/[^\r\n]/g, ' ');
|
|
179
|
+
return m.startsWith('<!--') ? `<${blank.slice(1, -1)}>` : blank;
|
|
180
|
+
});
|
|
164
181
|
}
|
|
165
182
|
|
|
166
183
|
function fromHtml({ source, rel }) {
|
|
@@ -193,7 +210,7 @@ function fromHtml({ source, rel }) {
|
|
|
193
210
|
|
|
194
211
|
// 3. <title> is the page's own name — always copy.
|
|
195
212
|
const titleRe = /<title>([^<]{2,200})<\/title>/gi;
|
|
196
|
-
while ((m = titleRe.exec(
|
|
213
|
+
while ((m = titleRe.exec(cleaned)) !== null) {
|
|
197
214
|
const titleStart = m.index + '<title>'.length;
|
|
198
215
|
out.push({ text: m[1].replace(/\s+/g, ' ').trim(), file: rel, line: lineAt(starts, m.index), origin: 'html-title', confidence: 'certain', ...trimmedSpan(m[1], titleStart) });
|
|
199
216
|
}
|
|
@@ -281,6 +281,12 @@ const GOVERNMENT_PINS = {
|
|
|
281
281
|
// task 1004523: the governor's docket — every matter awaiting a governor. The
|
|
282
282
|
// coarse gate is the room's; each source inside is scoped to its own atom.
|
|
283
283
|
'GET /government/docket': 'perm:board.vote.cast',
|
|
284
|
+
// task 1004529: who decides what — a change to the decision-rules table is an
|
|
285
|
+
// amendment by another door (same atom as proposing one), and a building's
|
|
286
|
+
// matter is put to its sitting by the same atom. Only a passed amendment
|
|
287
|
+
// writes the table; neither route does.
|
|
288
|
+
'POST /government/board/decision-rules': 'perm:board.item.open',
|
|
289
|
+
'POST /government/board/matters': 'perm:board.item.open',
|
|
284
290
|
};
|
|
285
291
|
|
|
286
292
|
// Module-scoped privileged-rank pins: a privileged route that lives in a FEATURE
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.10'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
package/tests/copy_inventory.mjs
CHANGED
|
@@ -98,6 +98,68 @@ test('stripEmbedded keeps line numbers truthful', () => {
|
|
|
98
98
|
assert.equal(copy.line, 6, 'a stripped script must not shift the lines after it — a wrong line number sends an artist to the wrong place');
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
+
// Task 1004545. street.html's header comment held a literal `<a>`, which opened a
|
|
102
|
+
// fake text node; under CRLF that run was 403 characters, under LF 396 — so a
|
|
103
|
+
// Windows regen dropped a row CI kept and redded "the committed artifacts match".
|
|
104
|
+
// The fixture rebuilds both halves: a tag inside a comment, and a run sized to
|
|
105
|
+
// straddle the cap.
|
|
106
|
+
// Eight 45-char lines under a 28-char head: 396 chars on LF, 404 on CRLF.
|
|
107
|
+
const NEAR_CAP_LINES = Array.from({ length: 8 }, () => ` ${'word '.repeat(8)}s.`);
|
|
108
|
+
const EOL_FIXTURE = [
|
|
109
|
+
'<!doctype html>',
|
|
110
|
+
'<!-- Author note: a building is a door (an <a>) to its interior.',
|
|
111
|
+
' The template is street.css, the <style> on the tokens.',
|
|
112
|
+
'-->',
|
|
113
|
+
'<style>.x { color: red; }</style>',
|
|
114
|
+
'<p>A real sentence on the page,',
|
|
115
|
+
...NEAR_CAP_LINES.slice(0, -1),
|
|
116
|
+
`${NEAR_CAP_LINES.at(-1)}</p>`,
|
|
117
|
+
'<p title="A tooltip a person reads">Second real line.</p>',
|
|
118
|
+
].join('\n');
|
|
119
|
+
|
|
120
|
+
test('LF and CRLF checkouts extract identical rows (task 1004545)', () => {
|
|
121
|
+
const lf = inv.fromHtml({ source: EOL_FIXTURE, rel: 'p.html' });
|
|
122
|
+
const crlf = inv.fromHtml({ source: EOL_FIXTURE.replace(/\n/g, '\r\n'), rel: 'p.html' });
|
|
123
|
+
const strip = (rows) => rows.map(({ text, line, origin }) => ({ text, line, origin }));
|
|
124
|
+
assert.deepEqual(strip(crlf), strip(lf), 'the registry must not depend on the checkout\'s line endings');
|
|
125
|
+
const run = EOL_FIXTURE.slice(EOL_FIXTURE.indexOf('A real'), EOL_FIXTURE.indexOf('</p>'));
|
|
126
|
+
assert.ok(run.length <= 400 && run.replace(/\n/g, '\r\n').length > 400, 'the fixture must straddle the cap, or this test proves nothing');
|
|
127
|
+
// The multi-line sentence sits under the cap only when CRLF counts as one
|
|
128
|
+
// character — so this is the case that reds if the length rule regresses.
|
|
129
|
+
assert.ok(lf.some((r) => r.text.startsWith('A real sentence on the page')), 'a multi-line run under the cap is copy on LF');
|
|
130
|
+
assert.ok(crlf.some((r) => r.text.startsWith('A real sentence on the page')), 'and the same run is copy on CRLF');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('HTML comments are never copy, and a tag named inside one swallows nothing', () => {
|
|
134
|
+
const rows = inv.fromHtml({ source: EOL_FIXTURE, rel: 'p.html' });
|
|
135
|
+
const texts = rows.map((r) => r.text);
|
|
136
|
+
assert.ok(!texts.some((t) => /Author note|its interior|the tokens/.test(t)), 'comment text is an author note, not page copy');
|
|
137
|
+
// The comment names `<style>`: a strip pass that ran style-first would blank from
|
|
138
|
+
// there to the real </style> — harmless here, but the same shape with <script>
|
|
139
|
+
// ahead of real copy would erase it. Both real lines must survive.
|
|
140
|
+
assert.ok(texts.includes('Second real line.'));
|
|
141
|
+
assert.ok(texts.includes('A tooltip a person reads'));
|
|
142
|
+
const real = rows.find((r) => r.text === 'Second real line.');
|
|
143
|
+
assert.equal(real.line, 15, 'a stripped comment must not shift the lines after it');
|
|
144
|
+
// A span still addresses the file's own characters (copy-apply writes through it).
|
|
145
|
+
assert.equal(EOL_FIXTURE.slice(real.start, real.end), 'Second real line.');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('a marker comment still splits copy into the segments the server drops independently', () => {
|
|
149
|
+
// The landing footer: the first sentence is apex-only, the second is not. One
|
|
150
|
+
// fused row would give both a new id and hide that a child instance drops half.
|
|
151
|
+
const src = '<p><!--apex-only:start-->A non-profit, capture-resistant commons. <!--apex-only:end-->Built by agents, on the platform.</p>';
|
|
152
|
+
const texts = inv.fromHtml({ source: src, rel: 'p.html' }).map((r) => r.text);
|
|
153
|
+
assert.ok(texts.includes('A non-profit, capture-resistant commons.'));
|
|
154
|
+
assert.ok(texts.includes('Built by agents, on the platform.'));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('a comment opener inside a script string does not eat the page after it', () => {
|
|
158
|
+
const src = '<script>const s = "<!--";</script>\n<p>Copy after the script.</p>\n<!-- x -->';
|
|
159
|
+
const texts = inv.fromHtml({ source: src, rel: 'p.html' }).map((r) => r.text);
|
|
160
|
+
assert.ok(texts.includes('Copy after the script.'));
|
|
161
|
+
});
|
|
162
|
+
|
|
101
163
|
test('the registry is deterministic and internally consistent', () => {
|
|
102
164
|
const a = inv.buildRegistry();
|
|
103
165
|
const b = inv.buildRegistry();
|
|
@@ -120,6 +120,9 @@ const stub = {
|
|
|
120
120
|
unassignRank: async () => true,
|
|
121
121
|
listRanksWithGrants: async () => [],
|
|
122
122
|
listAssignments: async () => [],
|
|
123
|
+
// task 1004529: People's row is read before a rank is given or taken. No rows
|
|
124
|
+
// = every building as today, so this matrix's walls are the route's own.
|
|
125
|
+
listDecisionRules: async () => [],
|
|
123
126
|
};
|
|
124
127
|
const REAL = {};
|
|
125
128
|
for (const k of Object.keys(stub)) {
|