@bongos/core 1.20.43 → 1.20.45
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 +118 -73
- 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 +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +57 -4
- package/docs/api-reference.md +7 -1
- package/docs/copy-inventory.md +46 -16
- package/docs/copy-registry.json +351 -20
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +3 -0
- package/docs/onboarding/slash-commands.md +12 -56
- package/docs/page-inventory.json +33 -4
- package/docs/page-readings.json +997 -962
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/collab.css +8 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands-lib.js +142 -0
- package/modules/hall-ui/public/commands.css +98 -0
- package/modules/hall-ui/public/commands.html +65 -0
- package/modules/hall-ui/public/commands.js +52 -0
- package/modules/hall-ui/public/commands.states.json +14 -0
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/oversight.css +0 -3
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +3 -0
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/commands.md +11 -0
- package/modules/specialities/skills.js +6 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/scripts/gds/module-artifact.js +52 -2
- package/scripts/gds/module.js +57 -4
- package/src/bongos/routes/cli-commands.js +49 -0
- package/src/bongos/routes/modules.js +5 -4
- package/src/bongos/routes.js +3 -0
- package/src/bongos/serve-internal.js +4 -1
- package/src/module-api.js +1 -1
- package/src/module-loader/manifest-schema.js +28 -0
- package/tests/hall_commands.mjs +162 -0
- package/tests/hall_lead_row_home.mjs +21 -0
- package/tests/hall_nav.mjs +3 -1
- package/tests/hall_page_gate_map.mjs +3 -0
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/module_manifest.mjs +22 -0
- package/tests/module_store_publish.mjs +88 -4
- package/tests/module_store_publish_route.mjs +20 -1
- package/tests/nav_permission_atoms.mjs +1 -1
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
43
43
|
|
|
44
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
44
|
+
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
45
45
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
46
46
|
<script src="/builders/palette.js" defer></script>
|
|
47
47
|
<script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
|
|
51
51
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
52
52
|
chrome exists before the page script runs. -->
|
|
53
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
53
|
+
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
54
54
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
55
55
|
<script src="/builders/palette.js" defer></script>
|
|
56
56
|
<!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
|
|
100
100
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
101
101
|
chrome exists before the page script runs. -->
|
|
102
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
102
|
+
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
103
103
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
104
104
|
<script src="/builders/palette.js" defer></script>
|
|
105
105
|
<!-- shared frontend toolkit (window.OTB) — loaded BEFORE sessions.js so it can pull from it. -->
|
|
@@ -400,7 +400,7 @@
|
|
|
400
400
|
|
|
401
401
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
402
402
|
chrome exists before the page scripts run. -->
|
|
403
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
403
|
+
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
404
404
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
405
405
|
<script src="/builders/palette.js" defer></script>
|
|
406
406
|
<!-- shared frontend toolkit (window.OTB) — loaded before consuming IIFEs. -->
|
|
@@ -303,6 +303,9 @@
|
|
|
303
303
|
// stays right whatever `branding.currency.label` is set to. Page is public
|
|
304
304
|
// like /ranks — 'signed' is the same cosmetic "show it once you're in".
|
|
305
305
|
{ id: 'economy', label: 'Economy', icon: 'docs', href: '/drachmae', gate: 'signed' },
|
|
306
|
+
// task 1004473: every skill and every bongos CLI verb, read live. Its page and
|
|
307
|
+
// both reads behind it are sign-in gated, so 'signed' is the real floor here.
|
|
308
|
+
{ id: 'commands', label: 'Commands', icon: 'docs', href: '/commands', gate: 'signed' },
|
|
306
309
|
{ id: 'diagrams', label: 'Diagrams', icon: 'docs', href: '/diagrams', gate: 'signed' },
|
|
307
310
|
{ id: 'atlas', label: 'Repo Atlas', icon: 'docs', href: '/atlas', gate: 'member' },
|
|
308
311
|
{ id: 'modules', label: 'Modules', icon: 'docs', href: '/modules', gate: 'member' },
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
|
|
177
177
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
178
178
|
|
|
179
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
179
|
+
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
180
180
|
<script src="/builders/palette.js" defer></script>
|
|
181
181
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
182
182
|
<!-- The kit, for its ONE renderer: the full-size ship-time visual figure
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
|
|
58
58
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
59
59
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
60
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
60
|
+
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
61
61
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
62
62
|
<script src="/builders/palette.js" defer></script>
|
|
63
63
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
</div>
|
|
139
139
|
</div>
|
|
140
140
|
|
|
141
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
141
|
+
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
142
142
|
<script src="/builders/palette.js" defer></script>
|
|
143
143
|
|
|
144
144
|
<script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
|
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
|
|
141
141
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
142
142
|
|
|
143
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
143
|
+
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
144
144
|
<script src="/builders/palette.js" defer></script>
|
|
145
145
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
146
146
|
<script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
|
|
163
163
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
164
164
|
chrome exists before the page script runs. -->
|
|
165
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
165
|
+
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
166
166
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
167
167
|
<script src="/builders/palette.js" defer></script>
|
|
168
168
|
<!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
|
|
172
172
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
173
173
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
174
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
174
|
+
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
175
175
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
176
176
|
<script src="/builders/palette.js" defer></script>
|
|
177
177
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# The Commands page, at /commands (task 1004473)
|
|
2
|
+
|
|
3
|
+
**Every skill and every `bongos` CLI verb on one Docs page, read from the live sources so it cannot drift.** The owner asked for it once task 1004471 hid the rarely used skills from Claude's menu: a skill nobody can see in the menu needs somewhere to be discovered. Five things it settled:
|
|
4
|
+
|
|
5
|
+
- **Two reads, no list in the page.** The skills come from `GET /specialities/installed-skills` (the specialities module's `installedSkills()`: core + instance + ENABLED-module skills, so a default-off module such as `pixel-art` or `design-styles` shows only where it is on); the verbs from the new core `GET /cli/commands`, which hands over `bin/bongos.js` `VERBS` grouped by its `GROUPS` — the order `bongos --help` prints. Core may not import a module, which is why the page asks for the two halves separately; each fails on its own, so a missing skills list does not take the CLI table down.
|
|
6
|
+
- **The only hand-written thing is the skill GROUP.** `commands-lib.js` `SKILL_GROUPS` places a skill by name or by the module it came from; a skill no group names lands under "Everything else", so a new skill always gets a row. Rows follow the order the group names them (the daily loop reads start → claim → ship), then A–Z.
|
|
7
|
+
- **"Shown to Claude" vs "Type it yourself" is `disable-model-invocation`.** `skills.js` now carries it (`typedOnly`), and `explainSkill()` reports `listed`. Only the literal `true` hides a skill, which is how Claude Code reads the key.
|
|
8
|
+
- **Sign-in is the whole gate.** The page joins `SIGNEDIN_BUILDERS_PAGE_RE`, its nav item is `signed` in Docs, and both reads are `requireBuilder`. Reading the list grants nothing (ADR 0310 §1): every command still meets the server's own rank checks when it runs.
|
|
9
|
+
- **At 760px and below each row is a small card.** Five columns cannot fit a phone and a sideways-scrolling table hides the costs, so `commands.css` stacks the cells under their column names (`data-label`). The CLI tables are `table-layout: fixed` with a 220px command column so the help lines align across the three groups.
|
|
10
|
+
|
|
11
|
+
Tests: `tests/hall_commands.mjs` (every SKILL.md and every VERBS entry exactly once, against the real tree; the typed-only marker per skill; the default-off modules absent; the route's gate). `docs/onboarding/slash-commands.md` now points here for the project's own list instead of keeping a second, hand-typed one.
|
|
@@ -92,6 +92,10 @@ function collect(out, dir, source) {
|
|
|
92
92
|
const entry = { name, description: typeof fm.description === 'string' ? fm.description : '', source };
|
|
93
93
|
for (const [key, field] of Object.entries(PEOPLE_KEYS)) if (typeof fm[key] === 'string' && fm[key]) entry[field] = fm[key];
|
|
94
94
|
if (Array.isArray(fm.requires) && fm.requires.length) entry.requires = fm.requires;
|
|
95
|
+
// `disable-model-invocation: true` (task 1004471) keeps a skill out of the
|
|
96
|
+
// menu Claude reads; a person still runs it by typing /name. Only the literal
|
|
97
|
+
// `true` hides it, which is how Claude Code reads the key (task 1004473).
|
|
98
|
+
if (fm['disable-model-invocation'] === 'true') entry.typedOnly = true;
|
|
95
99
|
out.set(name, entry);
|
|
96
100
|
}
|
|
97
101
|
}
|
|
@@ -150,6 +154,8 @@ function explainSkill(entry) {
|
|
|
150
154
|
commands,
|
|
151
155
|
needs: Array.isArray(e.requires) ? e.requires : [],
|
|
152
156
|
rank,
|
|
157
|
+
// Shown to Claude (it can start the skill from plain English) vs typed-only.
|
|
158
|
+
listed: e.typedOnly !== true,
|
|
153
159
|
derived: { does: !e.plain, reachFor: !e.reachFor, cost: !e.cost },
|
|
154
160
|
};
|
|
155
161
|
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.45",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.45",
|
|
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.20.
|
|
3
|
+
"version": "1.20.45",
|
|
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
|
@@ -8367,5 +8367,21 @@
|
|
|
8367
8367
|
"id": "1004471",
|
|
8368
8368
|
"text": "Claude's list of project commands is now short enough to read in full every session: the rarely used admin commands still work when you type them but no longer take up room, and one new /owner-review walks the backlog, bugs, i"
|
|
8369
8369
|
}
|
|
8370
|
+
],
|
|
8371
|
+
"1.20.44": [
|
|
8372
|
+
{
|
|
8373
|
+
"id": "1003637",
|
|
8374
|
+
"text": "The collab page's row layout rule now lives in the collab stylesheet instead of the shared oversight one."
|
|
8375
|
+
},
|
|
8376
|
+
{
|
|
8377
|
+
"id": "1004365",
|
|
8378
|
+
"text": "Every store module now comes with a how-to page, and the store refuses to publish one without it."
|
|
8379
|
+
}
|
|
8380
|
+
],
|
|
8381
|
+
"1.20.45": [
|
|
8382
|
+
{
|
|
8383
|
+
"id": "1004473",
|
|
8384
|
+
"text": "The Builders Hall now has a Commands page listing every slash command and every terminal command, with what each does, when to use it and what it costs, including the ones hidden from Claude's menu."
|
|
8385
|
+
}
|
|
8370
8386
|
]
|
|
8371
8387
|
}
|
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
// secrets), plus credential-named files anywhere in the module (SECRET_FILE_RE). Anything the denylist catches refuses the whole publish rather than being
|
|
23
23
|
// silently dropped: a module that ships without a file it expected is a broken module.
|
|
24
24
|
//
|
|
25
|
+
// THE HOW-TO GATE (ADR 0347 D4, task 1004365). A store version must carry HOWTO.md
|
|
26
|
+
// with the five required sections, each with some text. checkHowto is the ONE checker:
|
|
27
|
+
// `bongos module publish` runs it before uploading and the store's upload route runs
|
|
28
|
+
// it again through verifyModuleArtifact({ requireHowto: true }), so a hand-built upload
|
|
29
|
+
// can't skip it. It is opt-in there because install and update verify with the same
|
|
30
|
+
// function, and a version published before the gate existed must still install.
|
|
31
|
+
//
|
|
25
32
|
// Both halves live here so the CLI (packModule) and the store route
|
|
26
33
|
// (verifyModuleArtifact) cannot disagree about the format. Pure apart from reading the
|
|
27
34
|
// module directory in packModule; never touches the network or the database.
|
|
@@ -47,6 +54,42 @@ const NEVER_PACKED = new Set(['.upstream-signoff.json', INNER_MANIFEST_PATH]);
|
|
|
47
54
|
const MAX_TARBALL_BYTES = 5 * 1024 * 1024;
|
|
48
55
|
const READ_LIMITS = { maxUnpackedBytes: 16 * 1024 * 1024, maxFiles: 2000 };
|
|
49
56
|
|
|
57
|
+
const HOWTO_FILE = 'HOWTO.md';
|
|
58
|
+
// ADR 0347 D2 — level-2 headings, matched case-insensitively, in any order.
|
|
59
|
+
const HOWTO_SECTIONS = ['What it does', 'Install and enable', 'How to use it', 'Configuration', 'Limits and known issues'];
|
|
60
|
+
|
|
61
|
+
// Judge HOWTO.md's text (null = the file is absent). Reads headings only, never the
|
|
62
|
+
// writing: a section counts as filled when it holds any text once HTML comments (the
|
|
63
|
+
// scaffold's prompts) are removed. A heading inside a fenced code block doesn't count.
|
|
64
|
+
// Returns { ok, problems: [a plain sentence naming the file or the section] }.
|
|
65
|
+
function checkHowto(text) {
|
|
66
|
+
if (text == null) return { ok: false, problems: [`${HOWTO_FILE} is missing — every store module ships one (ADR 0347)`] };
|
|
67
|
+
const bodies = new Map();
|
|
68
|
+
let current = null;
|
|
69
|
+
let fence = null;
|
|
70
|
+
for (const line of String(text).replace(/<!--[\s\S]*?-->/g, '').split(/\r?\n/)) {
|
|
71
|
+
const f = line.match(/^\s*(```|~~~)/);
|
|
72
|
+
if (f) fence = fence === f[1] ? null : (fence || f[1]);
|
|
73
|
+
const h = !fence && !f && line.match(/^(#{1,6})\s+(.*?)\s*#*\s*$/);
|
|
74
|
+
if (h) {
|
|
75
|
+
// A level-1/2 heading starts a new section; a deeper one is structure, not text.
|
|
76
|
+
if (h[1].length <= 2) {
|
|
77
|
+
current = h[1].length === 2 ? h[2].toLowerCase() : null;
|
|
78
|
+
if (current !== null && !bodies.has(current)) bodies.set(current, '');
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (current !== null) bodies.set(current, bodies.get(current) + line.trim());
|
|
83
|
+
}
|
|
84
|
+
const problems = [];
|
|
85
|
+
for (const name of HOWTO_SECTIONS) {
|
|
86
|
+
const body = bodies.get(name.toLowerCase());
|
|
87
|
+
if (body === undefined) problems.push(`${HOWTO_FILE}: section "${name}" is missing`);
|
|
88
|
+
else if (!body) problems.push(`${HOWTO_FILE}: section "${name}" is empty`);
|
|
89
|
+
}
|
|
90
|
+
return { ok: problems.length === 0, problems };
|
|
91
|
+
}
|
|
92
|
+
|
|
50
93
|
function tarballName(key, version) { return `${ARTIFACT_NAME}-${key}-${version}.tgz`; }
|
|
51
94
|
|
|
52
95
|
// Every file under dir, as module-relative forward-slash paths, sorted. Skips
|
|
@@ -144,7 +187,9 @@ function packModule(key, { modulesDir, now = () => new Date(), modeOf } = {}) {
|
|
|
144
187
|
// `includeFiles: true` (install, task 1003785) also returns `files`: the module's own
|
|
145
188
|
// files — [{ path, mode, buf }], the inner manifest left out — from the SAME parse the
|
|
146
189
|
// hashes were checked against, so a caller that places them never re-reads the bytes.
|
|
147
|
-
|
|
190
|
+
// `requireHowto: true` (the store's upload route, ADR 0347 D4) also refuses a version
|
|
191
|
+
// whose HOWTO.md fails checkHowto.
|
|
192
|
+
async function verifyModuleArtifact(tgz, { key, maxBytes = MAX_TARBALL_BYTES, includeFiles = false, requireHowto = false } = {}) {
|
|
148
193
|
const fail = (code, message) => ({ ok: false, code, message });
|
|
149
194
|
if (!Buffer.isBuffer(tgz) || tgz.length === 0) return fail('empty_artifact', 'the upload is empty');
|
|
150
195
|
if (tgz.length > maxBytes) return fail('artifact_too_large', `the tarball is over ${maxBytes} bytes`);
|
|
@@ -204,6 +249,11 @@ async function verifyModuleArtifact(tgz, { key, maxBytes = MAX_TARBALL_BYTES, in
|
|
|
204
249
|
if (manifest.module_version !== moduleJson.version || manifest.core_version !== moduleJson.coreVersion) {
|
|
205
250
|
return fail('bad_manifest', 'the manifest version does not match module.json');
|
|
206
251
|
}
|
|
252
|
+
if (requireHowto) {
|
|
253
|
+
const howto = byName.get(HOWTO_FILE);
|
|
254
|
+
const h = checkHowto(howto ? howto.buf.toString('utf8') : null);
|
|
255
|
+
if (!h.ok) return fail('howto_incomplete', h.problems.join('; '));
|
|
256
|
+
}
|
|
207
257
|
|
|
208
258
|
const out = {
|
|
209
259
|
ok: true, manifest, moduleJson,
|
|
@@ -216,4 +266,4 @@ async function verifyModuleArtifact(tgz, { key, maxBytes = MAX_TARBALL_BYTES, in
|
|
|
216
266
|
return out;
|
|
217
267
|
}
|
|
218
268
|
|
|
219
|
-
module.exports = { MAX_TARBALL_BYTES, packModule, verifyModuleArtifact, deniedFiles };
|
|
269
|
+
module.exports = { MAX_TARBALL_BYTES, HOWTO_FILE, HOWTO_SECTIONS, packModule, verifyModuleArtifact, deniedFiles, checkHowto };
|
package/scripts/gds/module.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
//
|
|
9
9
|
// bongos module new <key>
|
|
10
10
|
// Scaffolds modules/<key>/ — a VALID, loader-discoverable module.json (M03),
|
|
11
|
-
// the dir skeleton (routes/, migrations/, ui/), a sample route factory,
|
|
12
|
-
// nested CLAUDE.md — with NO edit to any core file. The loader
|
|
11
|
+
// the dir skeleton (routes/, migrations/, ui/), a sample route factory, a
|
|
12
|
+
// HOWTO.md template for the store page (ADR 0347), and a nested CLAUDE.md — with NO edit to any core file. The loader
|
|
13
13
|
// (src/module-loader/loader.js) discovers it; enabling it (config/modules.json
|
|
14
14
|
// or env) mounts its routes behind the kernel's auth — the strangler endgame
|
|
15
15
|
// where adding a feature is "drop a directory," not "edit routes.js + modules.js".
|
|
@@ -94,6 +94,12 @@ function readJson(p) {
|
|
|
94
94
|
catch (e) { if (e.code === 'ENOENT') return null; throw e; }
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// A text file's contents, or null when it does not exist.
|
|
98
|
+
function readText(p) {
|
|
99
|
+
try { return fs.readFileSync(p, 'utf8'); }
|
|
100
|
+
catch (e) { if (e.code === 'ENOENT') return null; throw e; }
|
|
101
|
+
}
|
|
102
|
+
|
|
97
103
|
function envPrefix() {
|
|
98
104
|
const { FALLBACK_ENV_PREFIX } = require('../../src/instance-config');
|
|
99
105
|
try { return require('../../src/branding').branding().envPrefix || FALLBACK_ENV_PREFIX; }
|
|
@@ -213,9 +219,39 @@ Put \`NNN_*.sql\` under \`migrations/\` and set \`contributes.migrations: true\`
|
|
|
213
219
|
\`require('../../../src/module-api')\` — the doorway — is the ONLY core import
|
|
214
220
|
allowed. No deep core imports, no sibling-module imports; cooperate through
|
|
215
221
|
kernel seams (\`api.registerProvider\` / \`api.resolve\` / \`api.on\` / \`api.emit\`).
|
|
222
|
+
|
|
223
|
+
## The how-to (\`HOWTO.md\`)
|
|
224
|
+
This CLAUDE.md is for AI sessions working *inside* the module. \`HOWTO.md\` is for the
|
|
225
|
+
person who installs it — the store shows it as the module's page (ADR 0347). It is
|
|
226
|
+
plain Markdown, so any AI or person can write it. \`bongos module publish\` refuses
|
|
227
|
+
the module until each of its five sections has some text; replace the prompts in
|
|
228
|
+
\`<!-- -->\` with real answers. An optional \`howto.artifactUrl\` in module.json can
|
|
229
|
+
link a Claude page as an extra — never instead of the file.
|
|
216
230
|
`;
|
|
217
231
|
}
|
|
218
232
|
|
|
233
|
+
// The HOWTO.md a fresh module starts from (ADR 0347 D2): every required section as a
|
|
234
|
+
// heading, with its prompt in an HTML comment. checkHowto strips comments, so an
|
|
235
|
+
// untouched scaffold fails the publish gate until the author writes each section.
|
|
236
|
+
function howtoTemplate(key, manifest) {
|
|
237
|
+
const { HOWTO_SECTIONS } = require('./module-artifact');
|
|
238
|
+
const prompts = {
|
|
239
|
+
'What it does': 'A few sentences: what this module adds, and who it is for.',
|
|
240
|
+
'Install and enable': `How to get it (\`bongos module install ${key}\`) and switch it on (hall Modules tab).`,
|
|
241
|
+
'How to use it': 'Its commands, routes and hall surfaces, with one worked example.',
|
|
242
|
+
'Configuration': 'Env vars and settings, with their defaults. "None." is a fine answer.',
|
|
243
|
+
'Limits and known issues': 'What it does not do, and anything known to go wrong. "None known." is a fine answer.',
|
|
244
|
+
};
|
|
245
|
+
const sections = HOWTO_SECTIONS.map((name) => `## ${name}\n\n<!-- ${prompts[name]} -->\n`).join('\n');
|
|
246
|
+
return `# ${manifest.title} — how to use it
|
|
247
|
+
|
|
248
|
+
<!-- Written for the person who installs this module, or for any AI helping them.
|
|
249
|
+
The store shows this file as the module's page. Every section below must have
|
|
250
|
+
some text before \`bongos module publish\` will accept the module (ADR 0347). -->
|
|
251
|
+
|
|
252
|
+
${sections}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
219
255
|
// Has the core advanced past a deprecated module's declared removal version?
|
|
220
256
|
// `removeAfter: "1.12.0"` means "may be deleted once core is ABOVE 1.12.0".
|
|
221
257
|
function isPastRemoveAfter(maintenance, core) {
|
|
@@ -283,6 +319,7 @@ function scaffoldModule({ key, modulesDir = MODULES_DIR, core = coreVersion(), f
|
|
|
283
319
|
'module.json': JSON.stringify(manifest, null, 2) + '\n',
|
|
284
320
|
[`routes/${key}.js`]: routeTemplate(key),
|
|
285
321
|
'CLAUDE.md': claudeMdTemplate(key, manifest),
|
|
322
|
+
'HOWTO.md': howtoTemplate(key, manifest),
|
|
286
323
|
// git does not track empty dirs — keep the rest of the skeleton with placeholders.
|
|
287
324
|
'migrations/.gitkeep': '',
|
|
288
325
|
'ui/.gitkeep': '',
|
|
@@ -621,6 +658,7 @@ function cmdNew(args, { log = console.log, errlog = console.error } = {}) {
|
|
|
621
658
|
log(` • credit it — fill in author/origin/maintainer in module.json (license defaults to AGPL-3.0)`);
|
|
622
659
|
log(` • enable it — add "${key}": true to config/modules.json (or set <PREFIX>_MODULE_${ENVKEY}=1)`);
|
|
623
660
|
log(` • build it — fill routes/${key}.js; add migrations/ + ui/ (see modules/${key}/CLAUDE.md)`);
|
|
661
|
+
log(` • explain it — fill in every section of modules/${key}/HOWTO.md (publish refuses it until you do)`);
|
|
624
662
|
log(' • verify — bongos upgrade (checks coreVersion compatibility)');
|
|
625
663
|
return 0;
|
|
626
664
|
}
|
|
@@ -672,7 +710,7 @@ function cmdUpgrade(_args, { log = console.log, errlog = console.error, gather =
|
|
|
672
710
|
return 0;
|
|
673
711
|
}
|
|
674
712
|
|
|
675
|
-
function cmdCheck(args, { log = console.log, errlog = console.error, check = checkModulePublishability, sign = recordSignOff } = {}) {
|
|
713
|
+
function cmdCheck(args, { log = console.log, errlog = console.error, check = checkModulePublishability, sign = recordSignOff, howtoCheck } = {}) {
|
|
676
714
|
const key = args.find((a) => !a.startsWith('-'));
|
|
677
715
|
if (!key) {
|
|
678
716
|
errlog('usage: bongos module check <key> [--sign-off "Name <email>"]');
|
|
@@ -692,6 +730,13 @@ function cmdCheck(args, { log = console.log, errlog = console.error, check = che
|
|
|
692
730
|
for (const f of result.findings) errlog(` ✗ ${f.kind}: ${f.detail}`);
|
|
693
731
|
}
|
|
694
732
|
|
|
733
|
+
// The store's how-to gate (ADR 0347 D4), reported here so an author learns early.
|
|
734
|
+
// Advisory only: it gates `bongos module publish`, never an upstream submit.
|
|
735
|
+
const { checkHowto, HOWTO_FILE } = require('./module-artifact');
|
|
736
|
+
const howto = (howtoCheck || ((k) => checkHowto(readText(path.join(MODULES_DIR, k, HOWTO_FILE)))))(key);
|
|
737
|
+
if (howto.ok) log(` ✓ ${HOWTO_FILE} has every required section — ready for \`bongos module publish\`.`);
|
|
738
|
+
else for (const p of howto.problems) log(` ! ${p} — needed before \`bongos module publish\` (not for an upstream submit)`);
|
|
739
|
+
|
|
695
740
|
if (!result.ok) {
|
|
696
741
|
errlog(`\nREFUSING: "${key}" is not clear to submit upstream. Fix the finding(s) above and re-run.`);
|
|
697
742
|
return 1;
|
|
@@ -814,6 +859,14 @@ async function cmdPublish(args, {
|
|
|
814
859
|
catch (e) { errlog(`REFUSING: ${e.message}`); return 1; }
|
|
815
860
|
const { tgz, sidecar } = packed;
|
|
816
861
|
log(`bongos module publish — "${key}" ${sidecar.module_version} (needs core ${sidecar.core_version})`);
|
|
862
|
+
// The how-to gate (ADR 0347 D4) — the same checker the store re-runs on upload.
|
|
863
|
+
const howto = artifact.checkHowto(readText(path.join(modulesDir, key, artifact.HOWTO_FILE)));
|
|
864
|
+
if (!howto.ok) {
|
|
865
|
+
for (const p of howto.problems) errlog(` ✗ ${p}`);
|
|
866
|
+
errlog(`REFUSING: "${key}" needs a complete ${artifact.HOWTO_FILE} before it can be published — fill in the section(s) above.`);
|
|
867
|
+
return 1;
|
|
868
|
+
}
|
|
869
|
+
log(` ✓ ${artifact.HOWTO_FILE} has all ${artifact.HOWTO_SECTIONS.length} required sections`);
|
|
817
870
|
log(` ✓ packed ${sidecar.file_count} file(s), ${tgz.length} bytes — tree ${sidecar.tree_sha256.slice(0, 12)}…`);
|
|
818
871
|
if (tgz.length > artifact.MAX_TARBALL_BYTES) {
|
|
819
872
|
errlog(`REFUSING: the tarball is ${tgz.length} bytes, over the store's ${artifact.MAX_TARBALL_BYTES}-byte cap.`);
|
|
@@ -1130,7 +1183,7 @@ async function cmdUpdate(args, {
|
|
|
1130
1183
|
function printHelp(out = console.log) {
|
|
1131
1184
|
out('bongos module — scaffold + manage Cloud Bongos modules\n');
|
|
1132
1185
|
out('Usage:');
|
|
1133
|
-
out(' bongos module new <key> Scaffold modules/<key>/ (manifest + skeleton + sample route + CLAUDE.md)');
|
|
1186
|
+
out(' bongos module new <key> Scaffold modules/<key>/ (manifest + skeleton + sample route + HOWTO.md + CLAUDE.md)');
|
|
1134
1187
|
out(' bongos module list [--catalog] [--search <text>] [--json]');
|
|
1135
1188
|
out(' Browse the module catalog — what you can enable, with author + origin credit');
|
|
1136
1189
|
out(' bongos upgrade Pre-check enabled modules against the core version (fail-closed)');
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/bongos/routes/cli-commands.js — every `bongos <verb>` the CLI answers,
|
|
2
|
+
// for the hall's Commands page (task 1004473).
|
|
3
|
+
//
|
|
4
|
+
// WHY A ROUTE. The page is the one place a builder sees every skill AND every
|
|
5
|
+
// CLI command, and both halves must be read from their live source so the list
|
|
6
|
+
// cannot drift from what the tools actually do. The verb table is bin/bongos.js
|
|
7
|
+
// `VERBS` (grouped by its `GROUPS`, the order `bongos --help` prints); the
|
|
8
|
+
// browser cannot read that file, so this hands it over as data. The skills half
|
|
9
|
+
// comes from the specialities module's GET /specialities/installed-skills —
|
|
10
|
+
// core must not import a module, so the page asks for the two halves separately.
|
|
11
|
+
//
|
|
12
|
+
// Reading the list grants nothing (ADR 0310 §1): every verb still runs against
|
|
13
|
+
// the server's own rank checks. Sign-in is the floor only because the page it
|
|
14
|
+
// feeds sits on the hall's signed-in tier.
|
|
15
|
+
|
|
16
|
+
const express = require('express');
|
|
17
|
+
const auth = require('../auth');
|
|
18
|
+
// Side-effect-free: bin/bongos.js only dispatches when it is the main module.
|
|
19
|
+
const cli = require('../../../bin/bongos.js');
|
|
20
|
+
|
|
21
|
+
/** The CLI's verbs as `bongos --help` groups them: [{ key, label, commands: [{ verb, summary }] }]. */
|
|
22
|
+
function cliCommands(source = cli) {
|
|
23
|
+
const verbs = Object.entries(source.VERBS || {});
|
|
24
|
+
const groups = (source.GROUPS || []).map(([key, label]) => ({
|
|
25
|
+
key,
|
|
26
|
+
label,
|
|
27
|
+
commands: verbs.filter(([, def]) => def.group === key).map(([verb, def]) => ({ verb, summary: def.summary })),
|
|
28
|
+
}));
|
|
29
|
+
// A verb whose group `GROUPS` does not name still exists, so it still gets a row.
|
|
30
|
+
const known = new Set(groups.map((g) => g.key));
|
|
31
|
+
const stray = verbs.filter(([, def]) => !known.has(def.group));
|
|
32
|
+
if (stray.length) groups.push({ key: 'other', label: 'Other', commands: stray.map(([verb, def]) => ({ verb, summary: def.summary })) });
|
|
33
|
+
return groups.filter((g) => g.commands.length);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildCliCommandsRouter() {
|
|
37
|
+
const router = express.Router();
|
|
38
|
+
|
|
39
|
+
// GET /cli/commands — the bongos CLI's verbs, grouped like `bongos --help`.
|
|
40
|
+
// rank: any builder — read-only reference; the hall's Commands page is sign-in gated.
|
|
41
|
+
router.get('/cli/commands', auth.requireBuilder, (_req, res) => {
|
|
42
|
+
res.json({ version: cli.VERSION, groups: cliCommands() });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return router;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = buildCliCommandsRouter;
|
|
49
|
+
module.exports.cliCommands = cliCommands;
|
|
@@ -308,9 +308,10 @@ module.exports = function buildModulesRouter() {
|
|
|
308
308
|
// the store (task 1004271, ADR 0338 D1). Body: the gzip tarball
|
|
309
309
|
// `bongos module publish` builds (scripts/gds/module-artifact.js), sent as
|
|
310
310
|
// application/gzip. The route trusts nothing but the bytes: it recomputes every
|
|
311
|
-
// hash, re-runs the publish denylist
|
|
312
|
-
//
|
|
313
|
-
//
|
|
311
|
+
// hash, re-runs the publish denylist, validates module.json and checks that
|
|
312
|
+
// HOWTO.md has its required sections (ADR 0347 D4) itself (verifyModuleArtifact),
|
|
313
|
+
// then keeps the tarball on the control plane's disk and INSERTs the version row
|
|
314
|
+
// in one transaction (module-store.js). A key's first
|
|
314
315
|
// publish makes the caller its author; after that only the author may publish,
|
|
315
316
|
// a delisted key takes nothing new, and a version is published once and never
|
|
316
317
|
// changed.
|
|
@@ -333,7 +334,7 @@ module.exports = function buildModulesRouter() {
|
|
|
333
334
|
return res.fail('empty_artifact', { status: 400, message: 'Send the tarball as the body, Content-Type: application/gzip.' });
|
|
334
335
|
}
|
|
335
336
|
|
|
336
|
-
const v = await moduleArtifact.verifyModuleArtifact(tgz, { key });
|
|
337
|
+
const v = await moduleArtifact.verifyModuleArtifact(tgz, { key, requireHowto: true });
|
|
337
338
|
if (!v.ok) {
|
|
338
339
|
return res.fail(v.code, { status: v.code === 'artifact_too_large' ? 413 : 422, message: v.message });
|
|
339
340
|
}
|
package/src/bongos/routes.js
CHANGED
|
@@ -56,6 +56,8 @@ const buildSecurityRouter = require('./routes/security');
|
|
|
56
56
|
// the submit-for-review front door. Core (not module-owned): it manages
|
|
57
57
|
// EVERY module's enablement, including modules it must not import.
|
|
58
58
|
const buildModulesRouter = require('./routes/modules');
|
|
59
|
+
// The hall Commands page's CLI half (task 1004473): bin/bongos.js VERBS as data.
|
|
60
|
+
const buildCliCommandsRouter = require('./routes/cli-commands');
|
|
59
61
|
// The access-request ADMISSION routes carved to modules/onboarding/ (BV1.R81 /
|
|
60
62
|
// ADR 0093 §1); moduleLoader.mountModuleRoutes (below) mounts them via the
|
|
61
63
|
// onboarding module's route factory — no core require.
|
|
@@ -159,6 +161,7 @@ function buildGdsRouter() {
|
|
|
159
161
|
router.use(buildAuditLogRouter());
|
|
160
162
|
router.use(buildSecurityRouter());
|
|
161
163
|
router.use(buildModulesRouter());
|
|
164
|
+
router.use(buildCliCommandsRouter());
|
|
162
165
|
// access-requests (admission) is now mounted by the onboarding module via
|
|
163
166
|
// moduleLoader.mountModuleRoutes below (BV1.R81) — still BEFORE the public
|
|
164
167
|
// router, so its paths are byte-identical. The /sessions data plane is
|
|
@@ -1210,8 +1210,11 @@ function mountInternalSurfaces(app) {
|
|
|
1210
1210
|
// own room beside the studio, on the studio's tier. Its read (GET
|
|
1211
1211
|
// /copy-desk/pages/:pageId/draft) is any-builder like the .docx download, and
|
|
1212
1212
|
// every write is the page claim's holder only, checked server-side.
|
|
1213
|
+
// task 1004473: `commands`, the Commands page — every skill and every bongos CLI
|
|
1214
|
+
// verb. Both reads behind it are requireBuilder, so it sits on this tier;
|
|
1215
|
+
// reading the list grants nothing (ADR 0310 §1).
|
|
1213
1216
|
const SIGNEDIN_BUILDERS_PAGE_RE =
|
|
1214
|
-
/^\/builders\/(?:work|settings|atlas|modules|goals|goal-map|roadmap|copy-desk|studio|tweak-editor|ideas|thinking|collab|task|idea|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
|
|
1217
|
+
/^\/builders\/(?:work|settings|atlas|modules|goals|goal-map|roadmap|copy-desk|studio|tweak-editor|ideas|thinking|collab|task|idea|primer(?:\.md)?|diagrams|commands)(?:\.html)?\/?$/;
|
|
1215
1218
|
// The diagram image assets under /builders/diagrams/ ride the same tier as the
|
|
1216
1219
|
// page that embeds them.
|
|
1217
1220
|
const SIGNEDIN_BUILDERS_PREFIX_RE = /^\/builders\/diagrams\//;
|
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.20.
|
|
78
|
+
const CORE_VERSION = '1.20.45'; // 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');
|
|
@@ -53,6 +53,9 @@
|
|
|
53
53
|
// // actually declares in `contributes`, and every entry must be
|
|
54
54
|
// // one it actually lists there — a typo'd exemption is an error,
|
|
55
55
|
// // never a silent pass.
|
|
56
|
+
// "howto": { // OPTIONAL. Extras beside the module's HOWTO.md (ADR 0347 D3).
|
|
57
|
+
// "artifactUrl": "https://claude.ai/…" // A Claude page with the same how-to, shown as an extra,
|
|
58
|
+
// }, // never graded and never required — the file is the how-to.
|
|
56
59
|
// "provides": ["reward"], // OPTIONAL. seam PORTS this module registers a provider for.
|
|
57
60
|
// "consumes": ["grade"], // OPTIONAL. seam ports this module resolves (required capabilities).
|
|
58
61
|
// "prerequisites": { "modules": ["economy"] }, // OPTIONAL. other modules that must be enabled.
|
|
@@ -100,6 +103,17 @@ const ATTRIBUTION_KEYS = ['author', 'origin', 'license', 'maintainer'];
|
|
|
100
103
|
// Deliberately NOT exported: the validator and its own error message are the only
|
|
101
104
|
// readers, and an export nothing imports is dead weight the knip ratchet counts.
|
|
102
105
|
const SPEND_KEYS = ['requiresPayer'];
|
|
106
|
+
// The `howto` block (ADR 0347 D3) — closed for the same reason; not exported either.
|
|
107
|
+
const HOWTO_KEYS = ['artifactUrl'];
|
|
108
|
+
|
|
109
|
+
// An https link on claude.ai (or a subdomain). Shape only: the store never fetches it.
|
|
110
|
+
function isClaudeArtifactUrl(v) {
|
|
111
|
+
if (!isStr(v)) return false;
|
|
112
|
+
let u;
|
|
113
|
+
try { u = new URL(v); } catch { return false; }
|
|
114
|
+
const host = u.hostname.toLowerCase();
|
|
115
|
+
return u.protocol === 'https:' && !u.username && !u.password && (host === 'claude.ai' || host.endsWith('.claude.ai'));
|
|
116
|
+
}
|
|
103
117
|
|
|
104
118
|
const MAINTENANCE_STATUSES = ['core-maintained', 'maintained', 'deprecated', 'orphaned'];
|
|
105
119
|
const MAINTENANCE_KEYS = ['status', 'since', 'removeAfter', 'successor', 'note'];
|
|
@@ -306,6 +320,20 @@ function validateManifest(obj) {
|
|
|
306
320
|
}
|
|
307
321
|
}
|
|
308
322
|
|
|
323
|
+
// --- optional how-to extras (ADR 0347 D3) ---
|
|
324
|
+
if ('howto' in obj) {
|
|
325
|
+
const h = obj.howto;
|
|
326
|
+
if (!isObj(h)) E('howto: must be an object (e.g. { "artifactUrl": "https://claude.ai/…" })');
|
|
327
|
+
else {
|
|
328
|
+
for (const k of Object.keys(h)) {
|
|
329
|
+
if (!HOWTO_KEYS.includes(k)) E(`howto.${k}: unknown key (allowed: ${HOWTO_KEYS.join(', ')})`);
|
|
330
|
+
}
|
|
331
|
+
if ('artifactUrl' in h && !isClaudeArtifactUrl(h.artifactUrl)) {
|
|
332
|
+
E('howto.artifactUrl: must be an https:// link on claude.ai when present');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
309
337
|
// --- optional seam declarations ---
|
|
310
338
|
if ('provides' in obj && !isStrArray(obj.provides)) E('provides: must be an array of non-empty strings (port names)');
|
|
311
339
|
if ('consumes' in obj && !isStrArray(obj.consumes)) E('consumes: must be an array of non-empty strings (port names)');
|