@bongos/core 1.19.773 → 1.19.775
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 +195 -80
- 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 +5 -1
- package/clients/bongos-client/index.mjs +2 -0
- package/config/modules.neutral.json +1 -1
- package/docs/adr/0083-modular-architecture-strangler-build-plan.md +1 -0
- package/docs/adr/0177-goal-membership-kind.md +26 -2
- package/docs/adr/0181-teardown-state-gate-and-address-retention.md +2 -0
- package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -1
- package/docs/adr/0288-the-goal-wall-has-a-shared-floor-for-paths-no-module-owns.md +33 -0
- package/docs/adr/0289-an-unconsumed-contribution-is-a-ci-failure-not-a-boot-refusal.md +33 -0
- package/docs/adr/0290-disconnect-is-the-owner-initiated-exception-to-address-retention.md +40 -0
- package/docs/adr/README.md +3 -0
- package/docs/api/openapi.json +102 -11
- package/docs/api-reference.md +5 -4
- package/docs/copy-inventory.md +205 -190
- package/docs/copy-registry.json +435 -269
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +1 -1
- package/modules/builder-settings/render-prefs.js +281 -33
- package/modules/builder-settings/routes/settings.js +20 -12
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/dev-box/module.json +4 -0
- package/modules/hall-ui/public/settings-render.js +138 -0
- package/modules/hall-ui/public/settings.html +16 -9
- package/modules/hall-ui/public/settings.js +3 -70
- package/modules/lifecycle/ship-card.js +49 -13
- package/modules/platform-identity/migrations/platform_identity_021_drop_legacy_role.sql +94 -0
- package/modules/platform-identity/platform-identity.js +8 -19
- package/modules/platform-identity/project-catalog.js +24 -0
- package/modules/platform-identity/routes/sso.js +11 -14
- package/modules/platform-identity/tests/platform-identity.mjs +7 -9
- package/modules/provisioning/capacity-gate.js +54 -0
- package/modules/provisioning/catalog-bridge.js +2 -0
- package/modules/provisioning/disconnect.js +35 -0
- package/modules/provisioning/migrations/provisioning_016_disconnect.sql +21 -0
- package/modules/provisioning/module.json +1 -1
- package/modules/provisioning/provisioning.js +3 -3
- package/modules/provisioning/routes/disconnect.js +63 -0
- package/modules/provisioning/routes/provisioning.js +6 -6
- package/modules/provisioning/starter-bundles.js +32 -30
- package/modules/public-landing/public/projects.html +116 -11
- package/modules/public-landing/public/projects.probes.json +3 -0
- package/modules/ui-design/kit/serve.js +5 -5
- package/modules/ui-design/module.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/address-park-sweep.sh +78 -0
- package/scripts/gds/cli-lib.js +37 -0
- package/scripts/gds/fitness-migration-contraction.js +50 -0
- package/scripts/gds/fitness.js +18 -0
- package/scripts/gds/land-watch.js +8 -2
- package/scripts/gds/provision-config.js +4 -4
- package/scripts/gds/provision-disconnect.js +90 -0
- package/scripts/gds/provision-teardown.js +108 -0
- package/scripts/gds/provision.js +6 -65
- package/scripts/gds/seam-wiring-guard.js +273 -0
- package/scripts/gds/ship-finish.js +17 -10
- package/scripts/gds/ship-flow.js +5 -5
- package/scripts/gds/start.js +13 -7
- package/src/bongos/auth-admission.js +7 -8
- package/src/bongos/auth-config.js +3 -2
- package/src/bongos/goal-scope-check.js +23 -8
- package/src/bongos/module-scope-map.js +40 -0
- package/src/module-api.js +1 -1
- package/src/module-loader/manifest-schema.js +79 -1
- package/tests/catalog_only_client.mjs +8 -5
- package/tests/fitness.mjs +22 -0
- package/tests/goal_scope_check.mjs +63 -1
- package/tests/hall_settings_world.mjs +57 -1
- package/tests/hub_disconnect_ui.mjs +105 -0
- package/tests/land_watch.mjs +44 -0
- package/tests/membership_kind_lockstep.mjs +86 -27
- package/tests/module-scope-map.mjs +39 -0
- package/tests/module_exports_unique_keys.mjs +70 -0
- package/tests/module_manifest.mjs +85 -0
- package/tests/project_catalog_forget.mjs +83 -0
- package/tests/project_type_editable.mjs +8 -8
- package/tests/projects_hub.mjs +10 -4
- package/tests/projects_hub_pre_uat.mjs +5 -3
- package/tests/provision_disconnect.mjs +231 -0
- package/tests/provision_teardown_address.mjs +230 -0
- package/tests/provisioning_capacity_gate.mjs +192 -0
- package/tests/provisioning_disconnect.mjs +231 -0
- package/tests/provisioning_module_selection.mjs +14 -14
- package/tests/provisioning_recommendations.mjs +17 -25
- package/tests/provisioning_restart.mjs +5 -5
- package/tests/provisioning_starter_bundles.mjs +31 -9
- package/tests/public_landing_projects.mjs +4 -3
- package/tests/render_prefs.mjs +152 -0
- package/tests/seam_wiring_guard.mjs +234 -0
- package/tests/ship_card.mjs +67 -1
- package/tests/sso_checkin_legacy_field_gone.mjs +246 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// scripts/gds/seam-wiring-guard.js — Check 33: a declared contributes.<seam> that
|
|
2
|
+
// nothing consumes (task 1003819; successor to abandoned task 1002464).
|
|
3
|
+
//
|
|
4
|
+
// THE GAP THIS CLOSES. A module declares what it contributes in module.json.
|
|
5
|
+
// src/module-seams.js `verifyPortsSatisfied` checks the OPPOSITE direction — every
|
|
6
|
+
// `consumes` port has exactly one provider among the enabled modules — and
|
|
7
|
+
// src/module-loader/manifest-schema.js `validateManifest` checks the SHAPE of
|
|
8
|
+
// `contributes` (a closed key set, so a typo'd KEY errors). Neither asks whether a
|
|
9
|
+
// declared entry is actually WIRED. So a module can declare `contributes.pollers:
|
|
10
|
+
// ["ghost"]` with no modules/<key>/pollers/ghost.js and boot stays silent: the
|
|
11
|
+
// loader's startModulePollers treats a missing poller file as declarative-only and
|
|
12
|
+
// skips it, hallWidgetScripts skips a uiSection with no hall-widget.js, and
|
|
13
|
+
// moduleWebSurfaces skips a surface whose dir was never shipped. Every one of those
|
|
14
|
+
// skips is correct behaviour for a REAL declarative-only entry, and indistinguishable
|
|
15
|
+
// from a typo or a rename that left the manifest behind.
|
|
16
|
+
//
|
|
17
|
+
// WHAT "CONSUMED" MEANS. Each seam has one consumer in the tree, and each consumer
|
|
18
|
+
// looks for one artifact at a fixed path. SEAM_CONSUMERS below records that pairing —
|
|
19
|
+
// seam → the artifact its consumer resolves — so this guard asks the same question
|
|
20
|
+
// the consumer asks rather than inventing a second convention that could drift from
|
|
21
|
+
// it. A seam with NO consumer anywhere carries `consumer: null`, and every
|
|
22
|
+
// declaration of it is reported: that is the strongest form of the defect.
|
|
23
|
+
//
|
|
24
|
+
// WHY A CI GATE AND NOT A BOOT REFUSAL (ADR 0289). `verifyEnabledPorts` throws at
|
|
25
|
+
// boot because an unprovided port otherwise becomes a runtime throw on a request
|
|
26
|
+
// path, in a process that reported healthy — refusing early is strictly safer. For
|
|
27
|
+
// every seam BUT `routes`, an unconsumed contribution is not that shape: the consumer
|
|
28
|
+
// skips it gracefully, nothing crashes later, and the instance runs correctly minus
|
|
29
|
+
// one feature. Adding a production start-up failure for a manifest-hygiene defect
|
|
30
|
+
// would trade a real outage risk for a cosmetic one.
|
|
31
|
+
//
|
|
32
|
+
// `routes` is the exception and it argues the SAME way: mountModuleRoutes `require`s
|
|
33
|
+
// the factory with no try/catch, so a missing route file already takes the boot down
|
|
34
|
+
// today. There the guard is not a new gate at all — it moves an existing crash from
|
|
35
|
+
// production start-up to the build, which is strictly earlier and strictly cheaper.
|
|
36
|
+
// So CI covers both cases: the seams that fail silently get a voice, and the one that
|
|
37
|
+
// fails loudly gets to fail before it reaches a server.
|
|
38
|
+
//
|
|
39
|
+
// Exported for scripts/gds/fitness.js (checkSeamWiring) and for the unit suite, which
|
|
40
|
+
// drives scanSeamWiring() over synthetic manifests with an injected `exists` so every
|
|
41
|
+
// case is hermetic — the scanScopeMapSource / scanDeterministicBody precedent, where
|
|
42
|
+
// the pure scanner is the tested surface.
|
|
43
|
+
|
|
44
|
+
'use strict';
|
|
45
|
+
|
|
46
|
+
const fs = require('node:fs');
|
|
47
|
+
const path = require('node:path');
|
|
48
|
+
// The SAME enumerator + labeller validateManifest uses for the exemption cross-check,
|
|
49
|
+
// imported rather than re-declared: two copies would drift, and the drift would land
|
|
50
|
+
// on whether an exemption is honoured (task 1003819).
|
|
51
|
+
const { declaredEntries, entryLabel } = require('../../src/module-loader/manifest-schema.js');
|
|
52
|
+
|
|
53
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
54
|
+
|
|
55
|
+
// Is `child` inside `parent`? Both are repo-relative-or-absolute; normalised before
|
|
56
|
+
// comparing so a '..' segment cannot pass by spelling.
|
|
57
|
+
function within(parent, child) {
|
|
58
|
+
const a = path.resolve(REPO_ROOT, parent);
|
|
59
|
+
const b = path.resolve(REPO_ROOT, child);
|
|
60
|
+
const r = path.relative(a, b);
|
|
61
|
+
return r !== '' && !r.startsWith('..') && !path.isAbsolute(r);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// A repo-relative, forward-slashed path for an error message.
|
|
65
|
+
function rel(p) {
|
|
66
|
+
if (!p) return '(no artifact path)';
|
|
67
|
+
const r = path.relative(REPO_ROOT, path.isAbsolute(p) ? p : path.join(REPO_ROOT, p));
|
|
68
|
+
return r.split(path.sep).join('/');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// seam → how its consumer resolves a declared entry to an on-disk artifact.
|
|
72
|
+
// consumer the function that actually reads it, named so a reader can go check
|
|
73
|
+
// kind 'file' | 'dir' | 'registry' — what must exist
|
|
74
|
+
// artifact (moduleDir, entry) → path, or null when the seam is per-MODULE rather
|
|
75
|
+
// than per-entry (uiSections ships ONE widget however many it names)
|
|
76
|
+
// alternates optional extra paths that also count as wired
|
|
77
|
+
const SEAM_CONSUMERS = {
|
|
78
|
+
routes: {
|
|
79
|
+
consumer: 'src/module-loader/loader.js mountModuleRoutes',
|
|
80
|
+
kind: 'file',
|
|
81
|
+
artifact: (dir, entry) => path.join(dir, 'routes', `${entry}.js`),
|
|
82
|
+
},
|
|
83
|
+
pollers: {
|
|
84
|
+
consumer: 'src/module-loader/loader.js startModulePollers',
|
|
85
|
+
kind: 'file',
|
|
86
|
+
artifact: (dir, entry) => path.join(dir, 'pollers', `${entry}.js`),
|
|
87
|
+
},
|
|
88
|
+
rooms: {
|
|
89
|
+
// Allowed by the manifest schema since the doorway, but nothing in core reads
|
|
90
|
+
// contributes.rooms — the realtime seam was never wired to the loader. No module
|
|
91
|
+
// declares it today; if one ever does, this guard says so rather than letting the
|
|
92
|
+
// declaration read as though it did something.
|
|
93
|
+
consumer: null,
|
|
94
|
+
kind: 'file',
|
|
95
|
+
artifact: (dir, entry) => path.join(dir, 'rooms', `${entry}.js`),
|
|
96
|
+
},
|
|
97
|
+
uiSections: {
|
|
98
|
+
// Per-MODULE, not per-entry: hallWidgetScripts looks for one hall-widget.js
|
|
99
|
+
// however many sections the module names.
|
|
100
|
+
consumer: 'src/module-loader/loader.js hallWidgetScripts',
|
|
101
|
+
kind: 'file',
|
|
102
|
+
artifact: (dir) => path.join(dir, 'public', 'hall-widget.js'),
|
|
103
|
+
},
|
|
104
|
+
skills: {
|
|
105
|
+
// moduleSkillSources resolves a declared name against the module's OWN tree or
|
|
106
|
+
// its vendor/ subdir. A name that exists only in the core .claude/skills/ is
|
|
107
|
+
// skipped by it (`if (!src) continue`), so it is unconsumed BY THE MANIFEST —
|
|
108
|
+
// a legitimate declarative-only case, and what ui-design's exemption records.
|
|
109
|
+
//
|
|
110
|
+
// The artifact is the SKILL.md, not the directory: moduleSkillSources tests
|
|
111
|
+
// `fs.existsSync(path.join(p, 'SKILL.md'))`, so a skills/<name>/ dir without one
|
|
112
|
+
// is NOT a skill to it. Probing the bare directory would call such an entry wired
|
|
113
|
+
// while the real consumer skipped it — this guard's own defect, on the seam with
|
|
114
|
+
// the most declared entries.
|
|
115
|
+
consumer: 'scripts/gds/claude-materialize.js moduleSkillSources',
|
|
116
|
+
kind: 'file',
|
|
117
|
+
artifact: (dir, entry) => path.join(dir, 'skills', entry, 'SKILL.md'),
|
|
118
|
+
alternates: (dir, entry) => [path.join(dir, 'skills', 'vendor', entry, 'SKILL.md')],
|
|
119
|
+
},
|
|
120
|
+
disciplines: {
|
|
121
|
+
consumer: 'scripts/gds/discipline-modes.json (read by src/modules.js + claim.js)',
|
|
122
|
+
kind: 'registry',
|
|
123
|
+
artifact: () => null,
|
|
124
|
+
},
|
|
125
|
+
webSurfaces: {
|
|
126
|
+
consumer: 'src/module-loader/loader.js moduleWebSurfaces',
|
|
127
|
+
kind: 'dir',
|
|
128
|
+
// `entry` is the surface OBJECT here, not a string.
|
|
129
|
+
artifact: (dir, entry) => (entry && typeof entry.dir === 'string' ? path.join(dir, entry.dir) : null),
|
|
130
|
+
},
|
|
131
|
+
migrations: {
|
|
132
|
+
consumer: 'scripts/migrate.sh (the module-migration runner)',
|
|
133
|
+
kind: 'dir',
|
|
134
|
+
artifact: (dir) => path.join(dir, 'migrations'),
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Default existence probe — `kind` decides file-vs-directory. A relative path is
|
|
139
|
+
// resolved against the repo root so a manifest's `dir` may be either.
|
|
140
|
+
function fsExists(p, kind) {
|
|
141
|
+
try {
|
|
142
|
+
const st = fs.statSync(path.isAbsolute(p) ? p : path.join(REPO_ROOT, p));
|
|
143
|
+
return kind === 'dir' ? st.isDirectory() : st.isFile();
|
|
144
|
+
} catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// THE SCANNER. Pure over its inputs (the only I/O is the injected `exists`), so the
|
|
150
|
+
// unit suite drives every case with synthetic manifests and no tmp dirs.
|
|
151
|
+
//
|
|
152
|
+
// modules [{ key, dir, manifest }] — the loader's `discovered().loaded` shape
|
|
153
|
+
// exists (path, kind) => boolean — defaults to the real filesystem
|
|
154
|
+
// registryKeys Set of valid `disciplines` entries, or null to skip that seam
|
|
155
|
+
//
|
|
156
|
+
// Returns { ok, findings[], wired[], declarative[] }. `wired` and `declarative` are
|
|
157
|
+
// populated on the SUCCESS path too, deliberately: a guard that reports nothing when
|
|
158
|
+
// it passes leaves no evidence it ran, so the caller prints the counts either way.
|
|
159
|
+
function scanSeamWiring(modules = [], { exists = fsExists, registryKeys = null } = {}) {
|
|
160
|
+
const findings = [];
|
|
161
|
+
const wired = [];
|
|
162
|
+
const declarative = [];
|
|
163
|
+
|
|
164
|
+
for (const { key, dir, manifest } of modules) {
|
|
165
|
+
const contributes = (manifest && manifest.contributes) || {};
|
|
166
|
+
const exempt = (manifest && manifest.declarativeSeams) || {};
|
|
167
|
+
const isExempt = (seam, label) => Array.isArray(exempt[seam]) && exempt[seam].includes(label);
|
|
168
|
+
|
|
169
|
+
for (const [seam, value] of Object.entries(contributes)) {
|
|
170
|
+
const spec = SEAM_CONSUMERS[seam];
|
|
171
|
+
if (!spec) continue; // an unknown seam key is validateManifest's error, not ours
|
|
172
|
+
|
|
173
|
+
for (const entry of declaredEntries(seam, value)) {
|
|
174
|
+
const label = entryLabel(seam, entry);
|
|
175
|
+
|
|
176
|
+
if (spec.consumer === null) {
|
|
177
|
+
findings.push(`module "${key}" declares contributes.${seam}: "${label}", but NOTHING in the tree consumes the ${seam} seam at all — the declaration does nothing`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// A 'registry' seam is satisfied by membership in a roster, not by a file.
|
|
182
|
+
if (spec.kind === 'registry') {
|
|
183
|
+
if (registryKeys && !registryKeys.has(label)) {
|
|
184
|
+
findings.push(`module "${key}" declares contributes.${seam}: "${label}", but ${spec.consumer} has no such entry`);
|
|
185
|
+
} else {
|
|
186
|
+
wired.push({ key, seam, entry: label, via: spec.consumer });
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const primary = spec.artifact(dir, entry);
|
|
192
|
+
const candidates = [primary, ...(spec.alternates ? spec.alternates(dir, entry) : [])].filter(Boolean);
|
|
193
|
+
// An entry carrying path separators or '..' would aim the probe outside
|
|
194
|
+
// modules/<key>/. It is only an existence test over repo-committed content, so
|
|
195
|
+
// the blast radius is an on/off answer — but a declaration that reaches out of
|
|
196
|
+
// its own module is a finding in its own right, not something to resolve.
|
|
197
|
+
const escapes = candidates.find((p) => !within(dir, p));
|
|
198
|
+
if (escapes) {
|
|
199
|
+
findings.push(`module "${key}" declares contributes.${seam}: "${label}", which resolves to ${rel(escapes)} — outside the module's own directory. A contributes entry names something the module ships, so it may not contain a path separator or ".."`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const found = candidates.some((p) => exists(p, spec.kind));
|
|
203
|
+
|
|
204
|
+
if (found) {
|
|
205
|
+
// A STALE exemption is the same rot in the other direction: the artifact
|
|
206
|
+
// exists now, so the entry IS wired and the exemption reads as though a
|
|
207
|
+
// real gap were sanctioned. Fail it rather than let the list quietly lie.
|
|
208
|
+
if (isExempt(seam, label)) {
|
|
209
|
+
findings.push(`module "${key}" exempts ${seam} "${label}" as declarative-only, but ${rel(primary)} exists and ${spec.consumer} wires it — drop the stale declarativeSeams.${seam} entry`);
|
|
210
|
+
} else {
|
|
211
|
+
wired.push({ key, seam, entry: label, via: spec.consumer });
|
|
212
|
+
}
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (isExempt(seam, label)) {
|
|
217
|
+
declarative.push({ key, seam, entry: label });
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
findings.push(`module "${key}" declares contributes.${seam}: "${label}", but ${rel(primary)} does not exist — ${spec.consumer} silently skips it. Ship the artifact, drop the declaration, or record it in declarativeSeams.${seam} if it is declarative-only on purpose`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { ok: findings.length === 0, findings, wired, declarative };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// The roster `contributes.disciplines` is checked against. Returns null when the file
|
|
230
|
+
// is unreadable, which makes the disciplines seam UNCHECKED rather than failed — a
|
|
231
|
+
// missing registry is its own check's problem (fitness.js role packs), not this one's.
|
|
232
|
+
function disciplineKeys() {
|
|
233
|
+
try {
|
|
234
|
+
const raw = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'scripts', 'gds', 'discipline-modes.json'), 'utf8'));
|
|
235
|
+
return new Set(Object.keys(raw.modes || {}));
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// The fitness-check wrapper. Discovers every module.json through the loader — the same
|
|
242
|
+
// discovery the running server uses, so the guard cannot check a different module set
|
|
243
|
+
// than the one that boots.
|
|
244
|
+
function checkSeamWiring() {
|
|
245
|
+
const name = 'declared module seams are consumed (task 1003819)';
|
|
246
|
+
let modules = [];
|
|
247
|
+
try {
|
|
248
|
+
modules = require('../../src/module-loader/loader.js').discoverModules().loaded;
|
|
249
|
+
} catch (e) {
|
|
250
|
+
return {
|
|
251
|
+
name,
|
|
252
|
+
ok: true,
|
|
253
|
+
hardFail: false,
|
|
254
|
+
violations: [],
|
|
255
|
+
warnings: [`module discovery failed: ${e && e.message ? e.message : e}`],
|
|
256
|
+
note: 'module discovery unavailable — skipped.',
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const { ok, findings, wired, declarative } = scanSeamWiring(modules, { registryKeys: disciplineKeys() });
|
|
261
|
+
const seams = [...new Set(wired.map((w) => w.seam))].sort();
|
|
262
|
+
return {
|
|
263
|
+
name,
|
|
264
|
+
ok,
|
|
265
|
+
hardFail: !ok,
|
|
266
|
+
violations: findings,
|
|
267
|
+
warnings: [],
|
|
268
|
+
// Printed on the PASS path too — the wired set is the evidence the guard ran.
|
|
269
|
+
note: `${modules.length} module(s): ${wired.length} declared seam entr(ies) WIRED across ${seams.length} seam(s) (${seams.join(', ') || 'none'}), ${declarative.length} recorded declarative-only. A declared contributes.<seam> whose consumer's artifact is absent is a silent no-op (ADR 0289).`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = { checkSeamWiring, scanSeamWiring, SEAM_CONSUMERS, declaredEntries, entryLabel, disciplineKeys };
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
const fs = require('node:fs');
|
|
12
12
|
const os = require('node:os');
|
|
13
|
-
const { cliClient, printOnboardingNudge, rewardPolicy } = require('./cli-lib');
|
|
13
|
+
const { cliClient, printOnboardingNudge, rewardPolicy, cardFlagsFromMe } = require('./cli-lib');
|
|
14
14
|
const { closeoutPlan } = require('../../modules/lifecycle/ship-preflight');
|
|
15
15
|
const { syncMemoryOnShip } = require('./ship-memory-sync.js');
|
|
16
16
|
const { uploadSessionOnShip } = require('./ship-session-upload.js');
|
|
@@ -264,17 +264,18 @@ async function printCompletionCard({ taskId, postGradeStatus, card, usage, land
|
|
|
264
264
|
commit = (v && v.commit) ? String(v.commit).slice(0, 7) : (gitOk('git rev-parse --short HEAD') || null);
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
// Running drachmae total + the builder's
|
|
268
|
-
// both from one /me fetch (best-effort).
|
|
267
|
+
// Running drachmae total + the builder's card-richness level (task 1279's
|
|
268
|
+
// knob, a ladder since task 1002518), both from one /me fetch (best-effort).
|
|
269
269
|
let total = null;
|
|
270
|
-
let
|
|
270
|
+
let cardFlags = cardFlagsFromMe(null);
|
|
271
271
|
try {
|
|
272
272
|
const me = noteServerReward(await (await cliClient()).me.getMe());
|
|
273
273
|
if (me.ok && me.data) {
|
|
274
274
|
if (me.data.builder) total = me.data.builder.total_credits;
|
|
275
|
-
|
|
275
|
+
cardFlags = cardFlagsFromMe(me);
|
|
276
276
|
}
|
|
277
|
-
} catch (_) { /* total + knob are optional; default
|
|
277
|
+
} catch (_) { /* total + knob are optional; default to the standard rung */ }
|
|
278
|
+
const widgetsEnabled = cardFlags.showWidget;
|
|
278
279
|
|
|
279
280
|
// Token count + estimated cost from the session usage. Split the tokens into
|
|
280
281
|
// 'new' (fresh input + output + first-time cache writes) vs 'cached' re-reads
|
|
@@ -328,9 +329,12 @@ async function printCompletionCard({ taskId, postGradeStatus, card, usage, land
|
|
|
328
329
|
// Print the monospace card + write the branded HTML for the in-feed renderer
|
|
329
330
|
// (shared emitCard, task 1275). The skill reads the printed path and passes
|
|
330
331
|
// the contents to the visualization tool — the agent authors nothing, so the
|
|
331
|
-
// card is identical every ship. task
|
|
332
|
-
// emitCard
|
|
333
|
-
|
|
332
|
+
// card is identical every ship. task 1002518: the resolved richness level
|
|
333
|
+
// reaches emitCard as flags — at the plain rung no HTML is written or
|
|
334
|
+
// relayed, at the minimal rung not even the monospace card is printed (the
|
|
335
|
+
// guaranteed [otb-confirm] line always is), and at the rich rung the
|
|
336
|
+
// widget's summary arrives already open.
|
|
337
|
+
shipCard.emitCard(model, taskId, { flags: cardFlags });
|
|
334
338
|
|
|
335
339
|
// task 1687: in CI deploy mode this card is 'confirmed — landing' and the real
|
|
336
340
|
// confirmed->shipped land happens minutes later, server-side — so the SHIPPED
|
|
@@ -338,7 +342,10 @@ async function printCompletionCard({ taskId, postGradeStatus, card, usage, land
|
|
|
338
342
|
// land-watch hook renders the shipped card on a later prompt once it lands.
|
|
339
343
|
// Best-effort: a breadcrumb must never fail a ship (mirrors the card itself).
|
|
340
344
|
if (postGradeStatus === 'confirmed') {
|
|
341
|
-
|
|
345
|
+
// `cardFlags` carries the resolved rung so the later-rendered shipped card
|
|
346
|
+
// matches the one this session printed; `widgetsEnabled` stays beside it
|
|
347
|
+
// so a breadcrumb written by an older CLI still reads correctly.
|
|
348
|
+
try { require('./land-watch').writeBreadcrumb(taskId, { widgetsEnabled, cardFlags, cardInputs }); }
|
|
342
349
|
catch (_) { /* breadcrumb is best-effort */ }
|
|
343
350
|
}
|
|
344
351
|
} catch (_) {
|
package/scripts/gds/ship-flow.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
|
-
const { cliClient, requireSession, arg, detectWorktreeName } = require('./cli-lib');
|
|
11
|
+
const { cliClient, requireSession, arg, detectWorktreeName, cardFlagsFromMe } = require('./cli-lib');
|
|
12
12
|
const { resolveProse, markShipProgress, smokeGateMessages, proseFlagPresent, regradeNotesRefusal } = require('./ship-honesty.js');
|
|
13
13
|
const { classifyEmptyShip } = require('../../modules/lifecycle/ship-preflight');
|
|
14
14
|
const { scopeForModule, isModuleKey } = require('../../src/bongos/module-scope-map');
|
|
@@ -342,10 +342,10 @@ async function shipMain() {
|
|
|
342
342
|
removed,
|
|
343
343
|
sandboxUrl: sandboxGate.url,
|
|
344
344
|
});
|
|
345
|
-
// task 1279: honour the builder's
|
|
346
|
-
// fetched at the top of shipMain;
|
|
347
|
-
|
|
348
|
-
shipCard.emitCard(model, taskId, {
|
|
345
|
+
// task 1279 / 1002518: honour the builder's card-richness level (from the
|
|
346
|
+
// /me fetched at the top of shipMain; the standard rung if absent, and
|
|
347
|
+
// the boolean's old meaning against an older server).
|
|
348
|
+
shipCard.emitCard(model, taskId, { flags: cardFlagsFromMe(me) });
|
|
349
349
|
} catch (_) { /* the review pause stands with or without the card */ }
|
|
350
350
|
// #1061 polish: a sandbox review pause is a DELIBERATE, expected
|
|
351
351
|
// checkpoint — not a failed ship. printReviewBlock already showed the URL
|
package/scripts/gds/start.js
CHANGED
|
@@ -67,7 +67,7 @@ const { checkTree: _checkTree } = require('./tree-preflight.js');
|
|
|
67
67
|
if (_fault) { console.error(_fault); process.exit(1); }
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
const { cliClient, requireSession, fmtUsd, printOnboardingNudge, printCostWarning, printNeedsNudge, hallBase, API_BASE, rewardPolicy, costPlusOnlyNote, isDispatcherIncompatError } = require('./cli-lib');
|
|
70
|
+
const { cliClient, requireSession, fmtUsd, printOnboardingNudge, printCostWarning, printNeedsNudge, hallBase, API_BASE, rewardPolicy, costPlusOnlyNote, isDispatcherIncompatError, cardFlagsFromMe } = require('./cli-lib');
|
|
71
71
|
const { contextPack, formatContextPackCompact } = require('./context-pack');
|
|
72
72
|
const { depsFinding } = require('./preflight');
|
|
73
73
|
|
|
@@ -782,12 +782,18 @@ async function main() {
|
|
|
782
782
|
}
|
|
783
783
|
|
|
784
784
|
// ---------- widget (HTML for the app renderer) ----------
|
|
785
|
-
// task 1279: the per-builder
|
|
786
|
-
//
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
|
|
785
|
+
// task 1279 / 1002518: the per-builder card-richness level. Below the widget
|
|
786
|
+
// rung, even an explicit --widget request degrades to the markdown card below
|
|
787
|
+
// — so no HTML is emitted, read, or re-relayed (a real token saving, not just
|
|
788
|
+
// a visual change). /me carries the resolved level; an older instance carries
|
|
789
|
+
// only the boolean and cardFlagsFromMe reads either. Headless/cron callers
|
|
790
|
+
// don't pass --widget at all (no renderer), so they're unaffected.
|
|
791
|
+
//
|
|
792
|
+
// The `minimal` rung lands on the markdown card here rather than on nothing:
|
|
793
|
+
// the lifecycle card has a guaranteed one-line confirmation to fall back to,
|
|
794
|
+
// and this card — the list of what you can claim — has no such floor. Dropping
|
|
795
|
+
// it would leave a session opening with no answer to the question it asked.
|
|
796
|
+
const widgetsPref = me.ok ? cardFlagsFromMe(me).showWidget : true;
|
|
791
797
|
if (widget && widgetsPref) {
|
|
792
798
|
const myClaims = me.ok ? (me.data.active_claims || (me.data.active_claim ? [me.data.active_claim] : [])) : [];
|
|
793
799
|
// Private warnings (budget / onboarding next-stage) → stderr, so stdout
|
|
@@ -46,12 +46,13 @@ function membershipKind({ githubLogin } = {}) {
|
|
|
46
46
|
// Best-effort server-to-server membership check-in to the hub (ADR 0141 §3): "this
|
|
47
47
|
// github_id builds on project <client_id>, coarse membership <membership_kind>".
|
|
48
48
|
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
49
|
+
// ONE SPELLING ON THE WIRE: `membership_kind` (task 1003090 renamed it; task 1003092
|
|
50
|
+
// closed the cross-version window). During the window this sender wrote BOTH
|
|
51
|
+
// spellings, because the receiver is the HUB — a different instance on its own
|
|
52
|
+
// upgrade schedule — and new-field-only against an un-upgraded hub would have
|
|
53
|
+
// silently dropped the owner flag. The hub has been past that release since core
|
|
54
|
+
// 1.19.241 and no longer accepts the legacy field (its schema is strict), so
|
|
55
|
+
// sending it again would turn every check-in into a 400. Runs on every
|
|
55
56
|
// FEDERATED sign-in (the only sign-ins a federated instance has) so a CLI-only
|
|
56
57
|
// project still appears in the builder's My Projects with no hub browser session.
|
|
57
58
|
// Fire-and-forget + fully swallowed: a hub outage must NEVER fail a sign-in (mirrors
|
|
@@ -72,8 +73,6 @@ async function reportMembershipCheckIn({ githubId, githubLogin, displayName, ava
|
|
|
72
73
|
display_name: displayName ?? null,
|
|
73
74
|
avatar_url: avatarUrl ?? null,
|
|
74
75
|
membership_kind: kind,
|
|
75
|
-
// LEGACY (cross-version window): 'member' → 'builder' for an un-upgraded hub.
|
|
76
|
-
role: kind === 'member' ? 'builder' : kind,
|
|
77
76
|
}),
|
|
78
77
|
});
|
|
79
78
|
} catch (err) {
|
|
@@ -135,8 +135,9 @@ function idpConfigured() {
|
|
|
135
135
|
//
|
|
136
136
|
// 'member', not 'builder' (task 1003090, ADR 0177 §"the fifth use"): ADR 0174 reserves
|
|
137
137
|
// `role` — and the craft name 'Builder' — for Artist / Ideator / Builder, so the coarse
|
|
138
|
-
// project membership says what it means.
|
|
139
|
-
//
|
|
138
|
+
// project membership says what it means. It is the only spelling on the wire — the
|
|
139
|
+
// cross-version window that carried the legacy one closed under task 1003092; see
|
|
140
|
+
// reportMembershipCheckIn.
|
|
140
141
|
|
|
141
142
|
module.exports = {
|
|
142
143
|
SCOPE,
|
|
@@ -11,8 +11,11 @@
|
|
|
11
11
|
//
|
|
12
12
|
// 1. SCOPE-SUBSET — every `touches[]` element must be CONTAINED WITHIN the
|
|
13
13
|
// goal's territory (the union of its modules' globs, resolved via
|
|
14
|
-
// module-scope-map G01)
|
|
15
|
-
//
|
|
14
|
+
// module-scope-map G01) or in SHARED_GLOBS, the small set of roots no module
|
|
15
|
+
// can honestly own that every task legitimately writes (`tests/` — see the
|
|
16
|
+
// SHARED territory block in module-scope-map.js, task 1003655 / ADR 0288).
|
|
17
|
+
// This bounds *everyone*, regardless of rank: a goal member can only author
|
|
18
|
+
// work inside the goal's modules, plus that shared floor.
|
|
16
19
|
//
|
|
17
20
|
// 2. PROTECTED-PATH — a sub-Metic member (xenos/thetes) may not author a task
|
|
18
21
|
// touching the permission/ship-grade/deploy/migration core, even when that
|
|
@@ -37,7 +40,7 @@
|
|
|
37
40
|
'use strict';
|
|
38
41
|
|
|
39
42
|
const { matchOne } = require('./path-match');
|
|
40
|
-
const { globsForModules } = require('./module-scope-map');
|
|
43
|
+
const { globsForModules, SHARED_GLOBS } = require('./module-scope-map');
|
|
41
44
|
const { checkPermissionPaths } = require('./permission-path-check');
|
|
42
45
|
|
|
43
46
|
function clean(arr) {
|
|
@@ -54,13 +57,25 @@ function isCovered(touch, goalGlobs) {
|
|
|
54
57
|
return matchOne(touch, goalGlobs || []) !== null;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
// Wall 1 — scope subset. Pure: { ok, out_of_scope[], goal_globs[] }.
|
|
58
|
-
// `ok` is true iff every cleaned touch is covered by the goal's resolved globs
|
|
59
|
-
// An empty/unknown module set resolves to zero globs
|
|
60
|
+
// Wall 1 — scope subset. Pure: { ok, out_of_scope[], goal_globs[], shared_globs[] }.
|
|
61
|
+
// `ok` is true iff every cleaned touch is covered by the goal's resolved globs OR
|
|
62
|
+
// by the shared floor. An empty/unknown module set resolves to zero goal globs.
|
|
63
|
+
//
|
|
64
|
+
// `goal_globs` stays the MODULE-resolved set only — the shared floor is reported
|
|
65
|
+
// separately and never folded in. check()'s unknown_scope guard tests
|
|
66
|
+
// `goal_globs.length === 0`, so mixing the always-present shared globs into it
|
|
67
|
+
// would make a goal scoped to nothing but garbage resolve to a non-empty
|
|
68
|
+
// territory and slip past that fail-closed edge.
|
|
60
69
|
function scopeSubset(goalScopeModules, touches) {
|
|
61
70
|
const goalGlobs = globsForModules(clean(goalScopeModules));
|
|
62
|
-
const outOfScope = clean(touches)
|
|
63
|
-
|
|
71
|
+
const outOfScope = clean(touches)
|
|
72
|
+
.filter((p) => !isCovered(p, goalGlobs) && !isCovered(p, SHARED_GLOBS));
|
|
73
|
+
return {
|
|
74
|
+
ok: outOfScope.length === 0,
|
|
75
|
+
out_of_scope: outOfScope,
|
|
76
|
+
goal_globs: goalGlobs,
|
|
77
|
+
shared_globs: [...SHARED_GLOBS],
|
|
78
|
+
};
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
// Wall 2 — the protected-path delegate. Thin wrapper over checkPermissionPaths
|
|
@@ -388,6 +388,44 @@ const MODULE_GLOBS = {
|
|
|
388
388
|
],
|
|
389
389
|
};
|
|
390
390
|
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
// SHARED territory — the globs EVERY goal's wall implicitly includes (task 1003655).
|
|
393
|
+
//
|
|
394
|
+
// MODULE_GLOBS answers "who OWNS this path". Some paths have no honest owner: a
|
|
395
|
+
// shared root that every module writes into and none ships. `tests/` is the one
|
|
396
|
+
// that mattered — 751 flat suites covering all ~24 modules, owned by no key — so
|
|
397
|
+
// scopeSubset() denied EVERY touch under it. Since every real task adds a test,
|
|
398
|
+
// the create gate (POST /goals/:id/tasks) refused any task that honestly declared
|
|
399
|
+
// its own test file, whichever goal it was filed under. The wall only looked
|
|
400
|
+
// passable because callers under-declared their touches[] (idea 1000816).
|
|
401
|
+
//
|
|
402
|
+
// The two fixes we did NOT take, and why:
|
|
403
|
+
// • Give `tests/` to a key (devsecops, methodology). That is a FALSE ownership
|
|
404
|
+
// claim — an ideas-module test is the ideas module's work, not devsecops' —
|
|
405
|
+
// and it would hand one goal's members the whole suite while denying it to
|
|
406
|
+
// everyone else, which is worse than the gap.
|
|
407
|
+
// • Enumerate per-module test paths. The suites are flat and named by feature,
|
|
408
|
+
// not by module, and the glob format has no wildcards to carve with; the list
|
|
409
|
+
// would rot on the next test file.
|
|
410
|
+
//
|
|
411
|
+
// So `tests/` is SHARED, not owned: inside every goal's wall, outside every
|
|
412
|
+
// module's territory. That is sound because a test file is not a shipping
|
|
413
|
+
// surface — nothing imports it, so its blast radius on another module is zero —
|
|
414
|
+
// and the other two walls are untouched: claim collisions are still caught by
|
|
415
|
+
// touchesOverlap, and the protected-path wall (below) runs independently.
|
|
416
|
+
//
|
|
417
|
+
// FAIL-CLOSED, like `protected`: the exported list is DERIVED, not trusted. A
|
|
418
|
+
// shared glob is dropped if it overlaps PROTECTED_GLOBS, because a protected path
|
|
419
|
+
// in here would widen every goal's wall into the trust core in one line. Adding
|
|
420
|
+
// an entry is a deliberate design decision (ADR 0288) — the bar is "no module can
|
|
421
|
+
// honestly own it AND every task legitimately writes it", which today is `tests/`
|
|
422
|
+
// alone. Module-owned suites (modules/<key>/tests/) are already covered by their
|
|
423
|
+
// module's glob and do NOT belong here.
|
|
424
|
+
const SHARED_GLOBS_RAW = ['tests/'];
|
|
425
|
+
const SHARED_GLOBS = Object.freeze(
|
|
426
|
+
SHARED_GLOBS_RAW.filter((g) => !touchesOverlap([g], PROTECTED_GLOBS)),
|
|
427
|
+
);
|
|
428
|
+
|
|
391
429
|
// A glob set is protected iff it overlaps the imported PROTECTED_GLOBS — the one
|
|
392
430
|
// source of truth (permission-path-check.js). Prefix-aware both directions
|
|
393
431
|
// (path-match.touchesOverlap), so e.g. a module owning 'src/bongos/routes/inbox.js'
|
|
@@ -451,6 +489,8 @@ function scopeIncludesProtected(keys) {
|
|
|
451
489
|
module.exports = {
|
|
452
490
|
MODULE_GLOBS,
|
|
453
491
|
MODULE_SCOPE_MAP,
|
|
492
|
+
SHARED_GLOBS,
|
|
493
|
+
SHARED_GLOBS_RAW,
|
|
454
494
|
isProtected,
|
|
455
495
|
moduleKeys,
|
|
456
496
|
isModuleKey,
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.775'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|