@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.
- package/.bongos-core.json +57 -32
- package/.claude/skills/new-project/SKILL.md +2 -0
- 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 +4 -1
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0182-project-visibility-two-axes.md +1 -0
- package/docs/adr/0237-a-starter-bundle-is-a-preset-over-the-always-on-core-keyed-by-declared-type.md +137 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +47 -6
- package/docs/api-reference.md +4 -3
- package/docs/copy-inventory.md +210 -207
- package/docs/copy-registry.json +297 -269
- package/docs/module-api-changelog.md +4 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/platform-identity.js +30 -36
- package/modules/platform-identity/publish-gate.js +103 -0
- package/modules/platform-identity/routes/projects.js +31 -4
- package/modules/platform-identity/tests/platform-identity.mjs +7 -2
- package/modules/provisioning/routes/provisioning.js +70 -5
- package/modules/provisioning/starter-bundles.js +159 -0
- package/modules/public-landing/public/projects.html +157 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/projects_hub.mjs +62 -0
- package/tests/provisioning_starter_bundles.mjs +268 -0
- 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
|
+
});
|