@bongos/core 1.19.508 → 1.19.510

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 (30) hide show
  1. package/.bongos-core.json +57 -32
  2. package/.claude/skills/new-project/SKILL.md +2 -0
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +2 -0
  5. package/clients/bongos-client/index.cjs +2 -0
  6. package/clients/bongos-client/index.d.ts +4 -1
  7. package/clients/bongos-client/index.mjs +2 -0
  8. package/docs/adr/0182-project-visibility-two-axes.md +1 -0
  9. package/docs/adr/0237-a-starter-bundle-is-a-preset-over-the-always-on-core-keyed-by-declared-type.md +137 -0
  10. package/docs/adr/README.md +1 -0
  11. package/docs/api/openapi.json +47 -6
  12. package/docs/api-reference.md +4 -3
  13. package/docs/copy-inventory.md +210 -207
  14. package/docs/copy-registry.json +297 -269
  15. package/docs/module-api-changelog.md +4 -0
  16. package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
  17. package/modules/platform-identity/module.json +1 -1
  18. package/modules/platform-identity/platform-identity.js +30 -36
  19. package/modules/platform-identity/publish-gate.js +103 -0
  20. package/modules/platform-identity/routes/projects.js +31 -4
  21. package/modules/platform-identity/tests/platform-identity.mjs +7 -2
  22. package/modules/provisioning/routes/provisioning.js +70 -5
  23. package/modules/provisioning/starter-bundles.js +159 -0
  24. package/modules/public-landing/public/projects.html +157 -0
  25. package/package-lock.json +2 -2
  26. package/package.json +1 -1
  27. package/src/module-api.js +1 -1
  28. package/tests/projects_hub.mjs +62 -0
  29. package/tests/provisioning_starter_bundles.mjs +268 -0
  30. package/tests/publish_gate.mjs +167 -0
@@ -0,0 +1,167 @@
1
+ // tests/publish_gate.mjs — the PUBLISH GATE (task 1002335, BV1.R16; ADR 0182 D3/D4).
2
+ //
3
+ // A project is on the public map if and only if it has a name AND a description.
4
+ // That is the whole bar — identity only, deliberately not a completeness check —
5
+ // and until it is met the project is DARK MATTER: off the map and un-joinable.
6
+ //
7
+ // R04 (task 1002323) landed the DEFINITION, dormant. This file pins what R16 does
8
+ // with it, which is three things:
9
+ //
10
+ // 1. the gate BLOCKS the one publish action that could bypass it — an Archon
11
+ // featuring a row. ADR 0182 D3: `featured` may rank or promote among projects
12
+ // already on the map, it may never put one there;
13
+ // 2. every refusal NAMES what is absent (D4), in one shared phrase, because a
14
+ // project that cannot be published and is not told why is indistinguishable
15
+ // from a broken one;
16
+ // 3. the OTHER plane — `provisioning`, where an owner stands before their project
17
+ // is ever catalogued — asks the same gate through the kernel seam instead of
18
+ // keeping a second copy of the rule. The ADR 0083 one-way rule forbids the
19
+ // import; a duplicated two-field predicate is exactly the drift that the
20
+ // single SQL definition (publishableSql) exists to prevent.
21
+ //
22
+ // Pure + DB-free: the pool is a recording fake, and the seam is the real port object.
23
+ //
24
+ // Run: node --test tests/publish_gate.mjs
25
+
26
+ import assert from 'node:assert/strict';
27
+ import { test } from 'node:test';
28
+ import { createRequire } from 'node:module';
29
+
30
+ const require = createRequire(import.meta.url);
31
+ const pi = require('../modules/platform-identity/platform-identity.js');
32
+ const api = require('../src/module-api.js');
33
+
34
+ // The REAL port object, registered exactly as routes/projects.js registers it at
35
+ // boot — so provisioning below is answered by platform-identity's own gate, not by
36
+ // a stub that could agree with a rule nobody ships.
37
+ if (!api.hasProvider('project.publishGate')) {
38
+ api.registerProvider('project.publishGate', pi.publishGatePort);
39
+ }
40
+ const provRoutes = require('../modules/provisioning/routes/provisioning.js');
41
+
42
+ // A recording fake pool: returns queued responses in order, records every call.
43
+ function mockPool(responses = []) {
44
+ const calls = [];
45
+ let i = 0;
46
+ return {
47
+ calls,
48
+ query: async (text, params) => {
49
+ calls.push({ text: String(text), params: params || [] });
50
+ return responses[i++] || { rows: [], rowCount: 0 };
51
+ },
52
+ };
53
+ }
54
+
55
+ // ── what is missing, said the same way everywhere ────────────────────────────
56
+
57
+ test('missingPhrase names every absent field, so no surface invents its own wording', () => {
58
+ assert.equal(pi.missingPhrase(['description']), 'no description');
59
+ assert.equal(pi.missingPhrase(['name', 'description']), 'no name and no description');
60
+ // a met bar has nothing to name — the caller renders the "on the map" sentence
61
+ assert.equal(pi.missingPhrase([]), '');
62
+ assert.equal(pi.missingPhrase(undefined), '');
63
+ });
64
+
65
+ // ── the gate as a kernel seam port ───────────────────────────────────────────
66
+
67
+ test('the gate is published as a PORT, so another module can ask without importing', () => {
68
+ const port = pi.publishGatePort;
69
+ assert.deepEqual([...port.fields].sort(), ['description', 'name']);
70
+ assert.deepEqual(port.evaluate({ name: 'Mercury', description: 'A weather sim.' }),
71
+ { publishable: true, missing: [] });
72
+ assert.deepEqual(port.evaluate({ name: 'Mercury' }), { publishable: false, missing: ['description'] });
73
+ assert.equal(typeof port.missingPhrase, 'function');
74
+ // The port answers with the SAME function the SQL predicate mirrors — a caller
75
+ // that reached a private copy could drift from the feed's own definition.
76
+ assert.deepEqual(port.evaluate({ name: 'x', description: ' ' }),
77
+ pi.projectPublishability({ name: 'x', description: ' ' }));
78
+ });
79
+
80
+ test('the module manifest DECLARES the port — an undeclared provider is invisible to the boot check', () => {
81
+ const manifest = require('../modules/platform-identity/module.json');
82
+ assert.ok(manifest.provides.includes('project.publishGate'));
83
+ });
84
+
85
+ // ── featuring cannot publish an unpublishable project (ADR 0182 D3) ──────────
86
+
87
+ test('featuring is bounded IN THE UPDATE, not by a read-then-write', async () => {
88
+ const pool = mockPool([{ rows: [{ id: '7', origin: 'https://p', name: 'P', featured: true, status: 'active' }] }]);
89
+ const out = await pi.setProjectFeatured(7, true, { pool });
90
+ assert.equal(out.project.featured, true);
91
+ assert.equal(pool.calls.length, 1, 'the happy path is ONE statement — no separate check to race');
92
+ const sql = pool.calls[0].text;
93
+ assert.match(sql, /UPDATE platform_identity_projects SET featured = \$2/);
94
+ // the bound is the same predicate the public feed reads
95
+ assert.ok(sql.includes(pi.publishableSql()), 'the gate rides in the WHERE, from the one SQL definition');
96
+ assert.deepEqual(pool.calls[0].params, [7, true]);
97
+ });
98
+
99
+ test('featuring a DARK-MATTER project is refused, and the refusal names what is missing', async () => {
100
+ const pool = mockPool([
101
+ { rows: [] }, // the guarded UPDATE matched nothing
102
+ { rows: [{ id: '7', origin: 'https://p', name: 'P', description: null }] }, // …because of the gate
103
+ ]);
104
+ const out = await pi.setProjectFeatured(7, true, { pool });
105
+ assert.equal(out.blocked, true);
106
+ assert.deepEqual(out.missing, ['description'], 'ADR 0182 D4 — never merely refuse');
107
+ assert.ok(!out.project, 'nothing was featured');
108
+ });
109
+
110
+ test('a missing row is NOT reported as a gate refusal — the two answers are different', async () => {
111
+ const pool = mockPool([{ rows: [] }, { rows: [] }]);
112
+ const out = await pi.setProjectFeatured(999, true, { pool });
113
+ assert.equal(out.notFound, true);
114
+ assert.ok(!out.blocked, 'a curator told to "add a description" for a row that does not exist is sent chasing nothing');
115
+ });
116
+
117
+ test('UN-featuring is never gate-bound — a husk that is already featured must stay removable', async () => {
118
+ const pool = mockPool([{ rows: [{ id: '7', featured: false }] }]);
119
+ const out = await pi.setProjectFeatured(7, false, { pool });
120
+ assert.equal(out.project.featured, false);
121
+ // the predicate short-circuits on the OFF direction, in SQL, for every row
122
+ assert.match(pool.calls[0].text, /\$2 = false OR /);
123
+ assert.deepEqual(pool.calls[0].params, [7, false]);
124
+ });
125
+
126
+ // ── the other plane asks the same gate through the seam ──────────────────────
127
+
128
+ test('provisioning maps its own row onto the gate: an instance slug IS its name', () => {
129
+ const told = provRoutes.publishVerdict({ slug: 'mercury', description: 'A weather sim.' });
130
+ assert.equal(told.publishable, true);
131
+ assert.deepEqual(told.missing, []);
132
+
133
+ // The About step skipped: the only identity field this plane can lack.
134
+ const skipped = provRoutes.publishVerdict({ slug: 'mercury', description: null });
135
+ assert.equal(skipped.publishable, false);
136
+ assert.deepEqual(skipped.missing, ['description']);
137
+ assert.equal(skipped.missing_phrase, 'no description',
138
+ 'the phrase travels WITH the verdict so the owner-facing card cannot word the rule differently');
139
+
140
+ // A slug is never empty, so `name` can never be the missing half here — and the
141
+ // gate is never told about a column this table does not have.
142
+ assert.ok(!skipped.missing.includes('name'));
143
+ });
144
+
145
+ test('an instance read carries the verdict, so no read can ship the row without the reason', () => {
146
+ const row = { id: 4, slug: 'mercury', status: 'active', description: null, detail: {} };
147
+ const out = provRoutes.ownerInstance(row);
148
+ assert.equal(out.slug, 'mercury', 'the allow-listed projection is still there');
149
+ assert.equal(out.publish.publishable, false);
150
+ assert.deepEqual(out.publish.missing, ['description']);
151
+ });
152
+
153
+ test('with NO hub on the platform there is no map — and the verdict is silence, not a guess', () => {
154
+ // resolveOptional's absent case. platform-identity is hub-only (`default: false`),
155
+ // and the public map is a hub concept: an instance without it has nothing to be
156
+ // on. A locally-invented rule here is exactly what the seam exists to prevent, so
157
+ // the surfaces hide the section instead.
158
+ const seams = require('../src/module-seams.js');
159
+ const saved = seams.resolve('project.publishGate');
160
+ seams._reset();
161
+ try {
162
+ assert.equal(provRoutes.publishVerdict({ slug: 'mercury', description: null }), null);
163
+ assert.equal(provRoutes.ownerInstance({ id: 1, slug: 'mercury' }).publish, null);
164
+ } finally {
165
+ seams.registerProvider('project.publishGate', saved);
166
+ }
167
+ });