@bongos/core 1.21.163 → 1.21.164

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.
Files changed (35) hide show
  1. package/.bongos-core.json +54 -39
  2. package/.gitattributes +6 -0
  3. package/docs/copy-inventory.md +99 -99
  4. package/docs/copy-registry.json +149 -139
  5. package/docs/module-api-changelog.md +2 -0
  6. package/docs/page-readings.json +141 -306
  7. package/modules/copy-desk/page-status.js +3 -0
  8. package/modules/copy-desk/pages.js +5 -0
  9. package/modules/hall-ui/public/approval-queue-lib.js +4 -0
  10. package/modules/hall-ui/public/approval-queue.css +2 -0
  11. package/modules/hall-ui/public/approval-queue.js +1 -0
  12. package/modules/hall-ui/public/builders.js +6 -4
  13. package/modules/hall-ui/public/goal-inbox.js +100 -7
  14. package/modules/hall-ui/public/goals.js +2 -4
  15. package/modules/hall-ui/public/hall-render.js +39 -9
  16. package/modules/hall-ui/public/hall-widgets.js +20 -10
  17. package/modules/hall-ui/public/index.html +7 -0
  18. package/modules/hall-ui/public/style.css +95 -0
  19. package/modules/hall-ui/public/task-detail.js +8 -0
  20. package/modules/hall-ui/records/home-reshuffle.md +31 -0
  21. package/package-lock.json +2 -2
  22. package/package.json +1 -1
  23. package/release-notes.json +14 -0
  24. package/scripts/gds/copy-apply.js +65 -2
  25. package/scripts/gds/git-merge-regen.js +14 -0
  26. package/scripts/gds/page-reader.js +46 -0
  27. package/src/module-api.js +1 -1
  28. package/tests/conflict_resolve.mjs +6 -0
  29. package/tests/hall_approval_queue.mjs +16 -0
  30. package/tests/hall_board_world.mjs +7 -6
  31. package/tests/home_attention_cards.mjs +119 -0
  32. package/tests/home_bounded_reads.mjs +1 -1
  33. package/tests/promote_goal_id.mjs +8 -0
  34. package/tests/tweak_batch_apply.mjs +40 -0
  35. package/tests/tweak_land_heal.mjs +139 -0
@@ -0,0 +1,139 @@
1
+ // tests/tweak_land_heal.mjs — an approved page tweak lands without hand repair
2
+ // (task 1004775).
3
+ //
4
+ // Two approved page-tweak rounds branch from the same main and each re-read their
5
+ // own page, so both rewrite docs/page-readings.json — and both rewrite its top
6
+ // `counts` line, so git conflicted on every pair and the second round stranded as
7
+ // unresolvable_conflict until a person merged it. The heal: the otb-regen merge
8
+ // driver merges the file PER PAGE (page-reader.js mergeReadingDocs) and recomputes
9
+ // the counts, so the merge the conflict-resolve self-heal (and ship's own merge of
10
+ // main) runs comes out clean.
11
+ //
12
+ // 1. the pure rule: per page, one side's change wins; both sides changing one
13
+ // page differently stays a conflict, named;
14
+ // 2. WHOLE PATH: a real git repo wired with the real driver and .gitattributes
15
+ // rule; round A and round B branch from one main, A lands (main moves), B
16
+ // merges main exactly as the self-heal does — exit 0, no markers, both
17
+ // rounds' readings present, counts recomputed. No person involved;
18
+ // 3. the .gitattributes line exists (the driver only runs for paths it names).
19
+ //
20
+ // Run: node tests/tweak_land_heal.mjs
21
+
22
+ import { strict as assert } from 'node:assert';
23
+ import { createRequire } from 'node:module';
24
+ import fs from 'node:fs';
25
+ import os from 'node:os';
26
+ import path from 'node:path';
27
+ import { spawnSync } from 'node:child_process';
28
+ import { fileURLToPath } from 'node:url';
29
+ import { makeRunner } from './helpers.mjs';
30
+
31
+ const require = createRequire(import.meta.url);
32
+ const PR = require('../scripts/gds/page-reader.js');
33
+ const regen = require('../scripts/gds/git-merge-regen.js');
34
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
35
+ const { test, summary } = makeRunner();
36
+
37
+ function page(id, word) {
38
+ const lines = [{ key: 'L0001', section: 'main', text: `${word} heading`, placement: 'placed' }, { key: 'L0002', section: 'main', text: 'Skip', placement: 'shared' }];
39
+ return {
40
+ id, surface: 'landing', title: id, states_read: ['default'], files_hash: `f-${word}`, reading_hash: `r-${word}`,
41
+ counts: { lines: 2, placed: 1, shared: 1, unplaced: 0 }, omitted: { data: 0 }, notes: [], lines,
42
+ };
43
+ }
44
+ const doc = (pages) => PR.serialize(PR.buildDoc(pages));
45
+ const BASE = () => [page('landing:index', 'old'), page('landing:about', 'old'), page('landing:legal', 'old')];
46
+
47
+ await test('per page: each side\'s re-read is kept, counts recomputed', () => {
48
+ const base = BASE();
49
+ const ours = BASE(); ours[0] = page('landing:index', 'ours'); ours[0].counts.placed = 5; ours[0].counts.lines = 6;
50
+ const theirs = BASE(); theirs[1] = page('landing:about', 'theirs');
51
+ const m = PR.mergeReadingDocs({ base: doc(base), ours: doc(ours), theirs: doc(theirs) });
52
+ assert.equal(m.ok, true);
53
+ const out = JSON.parse(m.content);
54
+ assert.deepEqual(out.pages.map((p) => p.reading_hash), ['r-ours', 'r-theirs', 'r-old']);
55
+ assert.equal(out.counts.placed, 7, 'counts are summed from the merged pages');
56
+ assert.equal(m.content, doc([ours[0], theirs[1], base[2]]), 'byte-identical to what the reader would write');
57
+ });
58
+
59
+ await test('both sides re-reading ONE page differently stays a conflict, named', () => {
60
+ const ours = BASE(); ours[0] = page('landing:index', 'ours');
61
+ const theirs = BASE(); theirs[0] = page('landing:index', 'theirs');
62
+ const m = PR.mergeReadingDocs({ base: doc(BASE()), ours: doc(ours), theirs: doc(theirs) });
63
+ assert.equal(m.ok, false);
64
+ assert.deepEqual(m.conflicted, ['landing:index']);
65
+ assert.equal(regen.regenerateResolved('docs/page-readings.json', { base: doc(BASE()), ours: doc(ours), theirs: doc(theirs) }).ok, false);
66
+ });
67
+
68
+ await test('a page only one side added lands where that side put it; a removed page stays removed', () => {
69
+ const theirs = [...BASE().slice(0, 1), page('landing:new', 'x'), ...BASE().slice(1)];
70
+ const ours = BASE().filter((p) => p.id !== 'landing:legal');
71
+ const m = PR.mergeReadingDocs({ base: doc(BASE()), ours: doc(ours), theirs: doc(theirs) });
72
+ assert.equal(m.ok, true);
73
+ assert.deepEqual(JSON.parse(m.content).pages.map((p) => p.id), ['landing:index', 'landing:new', 'landing:about']);
74
+ });
75
+
76
+ await test('.gitattributes routes docs/page-readings.json through the otb-regen driver', () => {
77
+ const attrs = fs.readFileSync(path.join(ROOT, '.gitattributes'), 'utf8');
78
+ assert.match(attrs, /^docs\/page-readings\.json\s+merge=otb-regen\s*$/m);
79
+ assert.equal(regen.REGENERABLE.has('docs/page-readings.json'), true);
80
+ });
81
+
82
+ await test('WHOLE PATH: two rounds from one main both land; the second merges main with no person', () => {
83
+ const driver = path.join(ROOT, 'scripts/gds/git-merge-regen.js').replace(/\\/g, '/');
84
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-tweak-land-'));
85
+ const g = (...a) => {
86
+ const r = spawnSync('git', a, { cwd: tmp, encoding: 'utf8' });
87
+ return { status: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
88
+ };
89
+ const RJ = path.join(tmp, 'docs', 'page-readings.json');
90
+ const write = (pages, html) => {
91
+ fs.writeFileSync(RJ, doc(pages));
92
+ for (const [f, s] of Object.entries(html || {})) fs.writeFileSync(path.join(tmp, f), s);
93
+ };
94
+ try {
95
+ g('init', '-q', '-b', 'main');
96
+ g('config', 'user.email', 't@t'); g('config', 'user.name', 't');
97
+ g('config', 'core.autocrlf', 'false');
98
+ g('config', 'merge.otb-regen.driver', `node "${driver}" %O %A %B %P`);
99
+ fs.writeFileSync(path.join(tmp, '.gitattributes'), 'docs/page-readings.json merge=otb-regen\n');
100
+ fs.mkdirSync(path.join(tmp, 'docs'));
101
+ write(BASE(), { 'index.html': 'Old index heading\n', 'about.html': 'Old about heading\n' });
102
+ g('add', '-A'); g('commit', '-qm', 'base');
103
+
104
+ // Round A (landing:index) and round B (landing:about), both applied from this main.
105
+ g('checkout', '-qb', 'round-a');
106
+ const a = BASE(); a[0] = page('landing:index', 'A'); a[0].counts.placed = 3;
107
+ write(a, { 'index.html': 'New index heading\n' });
108
+ g('add', '-A'); g('commit', '-qm', 'round A applied');
109
+ g('checkout', '-q', 'main');
110
+ g('checkout', '-qb', 'round-b');
111
+ const b = BASE(); b[1] = page('landing:about', 'B'); b[1].counts.placed = 4;
112
+ write(b, { 'about.html': 'New about heading\n' });
113
+ g('add', '-A'); g('commit', '-qm', 'round B applied');
114
+
115
+ // Round A lands: main moves.
116
+ g('checkout', '-q', 'main');
117
+ assert.equal(g('merge', '-q', '--no-ff', '--no-edit', 'round-a').status, 0);
118
+
119
+ // Before this task: the counts line conflicts. Now: the self-heal's merge is clean.
120
+ g('checkout', '-q', 'round-b');
121
+ const merge = g('merge', '--no-edit', 'main');
122
+ assert.equal(merge.status, 0, `round B must merge main clean; got ${merge.status} :: ${merge.out}`);
123
+ assert.equal(g('diff', '--name-only', '--diff-filter=U').out.trim(), '');
124
+ const healed = fs.readFileSync(RJ, 'utf8');
125
+ assert.ok(!regen.hasMarkers(healed), 'no conflict markers');
126
+ const out = JSON.parse(healed);
127
+ assert.deepEqual(out.pages.map((p) => p.reading_hash), ['r-A', 'r-B', 'r-old'], 'both rounds\' readings are present');
128
+ assert.equal(out.counts.placed, 3 + 4 + 1, 'counts regenerated from the merged pages');
129
+ assert.equal(healed, doc([a[0], b[1], BASE()[2]]));
130
+
131
+ // And round B then lands on main as a fast-forward of what the self-heal pushed.
132
+ g('checkout', '-q', 'main');
133
+ assert.equal(g('merge', '-q', '--ff-only', 'round-b').status, 0, 'round B lands');
134
+ } finally {
135
+ fs.rmSync(tmp, { recursive: true, force: true });
136
+ }
137
+ });
138
+
139
+ summary();