@bongos/core 1.19.773 → 1.19.774
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 +189 -74
- 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/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 +2 -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/public-landing/public/projects.html +116 -11
- package/modules/public-landing/public/projects.probes.json +3 -0
- package/modules/ui-design/kit/serve.js +3 -2
- 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/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_restart.mjs +5 -5
- 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
|
@@ -58,7 +58,8 @@ const PROJECT_TYPES = ['game', 'research', 'business', 'non-profit', 'not-sure']
|
|
|
58
58
|
// instance whose liveness probe says it is down — the settings-apply leg minus the
|
|
59
59
|
// env patch. Its own action because no existing one fits: provision refuses
|
|
60
60
|
// 'active' and settings-apply requires a value that actually changed.
|
|
61
|
-
|
|
61
|
+
// 'disconnect' (task 1003249, migration provisioning_016): the owner's true delete — ./disconnect.js.
|
|
62
|
+
const INTENT_ACTIONS = ['provision', 'teardown', 'oauth-manifest', 'domain-attach', 'settings-apply', 'restart', 'disconnect'];
|
|
62
63
|
// Every state a teardown can be asked for — i.e. everything except the one
|
|
63
64
|
// state where there is provably nothing left to release. DERIVED from
|
|
64
65
|
// INSTANCE_STATES on purpose (task 1003042, walk finding N5): the old hand-kept
|
|
@@ -1489,8 +1490,7 @@ module.exports = {
|
|
|
1489
1490
|
listActiveInstancesForLiveness, recordLivenessProbe, recordDnsNote, clearErrorNote,
|
|
1490
1491
|
// data access — the owed-push marker (task 1003524)
|
|
1491
1492
|
recordSettingsPushOwed,
|
|
1492
|
-
createInstance, setInstanceStatus, updateInstanceDomain, updateInstanceSettings, updateInstanceDetail,
|
|
1493
|
-
createInstance, setInstanceStatus, updateInstanceDomain, clearReleasedDomains, updateInstanceSettings,
|
|
1493
|
+
createInstance, setInstanceStatus, updateInstanceDomain, clearReleasedDomains, updateInstanceSettings, updateInstanceDetail,
|
|
1494
1494
|
enqueueIntent, enqueueTeardown, enqueueSettingsApply, enqueueRestart, getOpenIntent, claimNextIntent, resolveIntent,
|
|
1495
1495
|
recordEvent, instanceStateForSlug, planetChoicesForHosts, projectOwnedBy, costLedger,
|
|
1496
1496
|
// data access — GitHub App Manifest attempts (task 2080)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// modules/provisioning/routes/disconnect.js — POST /provisioning/instances/:id/disconnect
|
|
2
|
+
// (task 1003249, ADR 0290: "Disconnect from Bongos entirely").
|
|
3
|
+
//
|
|
4
|
+
// The web tier's half of the owner's true delete. Like every provisioning action it only
|
|
5
|
+
// ENQUEUES; the control-plane runner does the destruction (ADR 0111 §2). Its own routes
|
|
6
|
+
// file (declared in module.json) because routes/provisioning.js sits at the size ratchet.
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const express = require('express');
|
|
10
|
+
const api = require('../../../src/module-api');
|
|
11
|
+
const { pool, validateOrRespond, parseId } = api;
|
|
12
|
+
const provisioning = require('../provisioning');
|
|
13
|
+
const disconnect = require('../disconnect');
|
|
14
|
+
|
|
15
|
+
const log = api.logger('provisioning');
|
|
16
|
+
|
|
17
|
+
module.exports = function buildDisconnectRouter() {
|
|
18
|
+
const router = express.Router();
|
|
19
|
+
|
|
20
|
+
// POST /provisioning/instances/:id/disconnect — rank: any authenticated builder (own
|
|
21
|
+
// resource); an archon may disconnect any project. 404 for anyone else, so another
|
|
22
|
+
// owner's id is never confirmed. The typed name is checked HERE, not only in the page:
|
|
23
|
+
// this erases a database, so a replayed or scripted request must carry the same proof
|
|
24
|
+
// the button does. No status gate — any state may be disconnected; the queue refuses
|
|
25
|
+
// while another run is open (disconnect.enqueueDisconnect).
|
|
26
|
+
router.post('/provisioning/instances/:id/disconnect', api.requireBuilder, async (req, res) => {
|
|
27
|
+
const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
|
|
28
|
+
if (!id) return;
|
|
29
|
+
if (validateOrRespond(req, res, { confirm_slug: { required: true, type: 'string', maxLength: 40 } })) return;
|
|
30
|
+
try {
|
|
31
|
+
const inst = await provisioning.getInstanceById(pool, id);
|
|
32
|
+
const isOwner = !!inst && String(inst.owner_builder_id) === String(req.builder.id);
|
|
33
|
+
if (!inst || (!isOwner && req.builder.rank !== 'archon')) return res.fail('instance_not_found', 404);
|
|
34
|
+
if (String(req.body.confirm_slug).trim().toLowerCase() !== inst.slug) {
|
|
35
|
+
return res.fail('confirm_mismatch', { status: 400,
|
|
36
|
+
message: `Type the project's name exactly — ${inst.slug} — to disconnect it. Nothing was changed.` });
|
|
37
|
+
}
|
|
38
|
+
const actor = isOwner ? 'api:self' : `api:archon:${req.builder.id}`;
|
|
39
|
+
const r = await disconnect.enqueueDisconnect(pool, id, actor);
|
|
40
|
+
if (r.conflict) {
|
|
41
|
+
return res.fail('intent_conflict', { status: 409,
|
|
42
|
+
message: `A '${r.intent.action}' run is ${r.intent.state} for this project. Disconnecting waits for it to finish — try again once it has.`,
|
|
43
|
+
details: { action: r.intent.action, state: r.intent.state } });
|
|
44
|
+
}
|
|
45
|
+
if (r.created) {
|
|
46
|
+
await provisioning.recordEvent(pool, {
|
|
47
|
+
instanceId: id, ownerBuilderId: inst.owner_builder_id, event: 'disconnect',
|
|
48
|
+
detail: isOwner ? 'disconnect requested by owner' : `disconnect requested by archon ${req.builder.github_login || req.builder.id}`,
|
|
49
|
+
actor,
|
|
50
|
+
}).catch(() => {});
|
|
51
|
+
}
|
|
52
|
+
res.json({ ok: true, queued: r.created, action: 'disconnect', status: inst.status,
|
|
53
|
+
message: r.created
|
|
54
|
+
? 'Queued. The platform takes the project down, erases its database and backups, and releases its name and address. It leaves your list when that is done.'
|
|
55
|
+
: 'A disconnect is already queued for this project.' });
|
|
56
|
+
} catch (err) {
|
|
57
|
+
log.error('[provisioning] POST /provisioning/instances/:id/disconnect', err);
|
|
58
|
+
res.fail('disconnect_failed', { status: 500, message: 'internal error' });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return router;
|
|
63
|
+
};
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// only shape that bills; ../paid-shape-gate.js.
|
|
18
18
|
// An `adopt` ALSO passes the credential
|
|
19
19
|
// preflight — ../credential-preflight.js,
|
|
20
|
-
// task 1002909; greenfield is exempt)
|
|
20
|
+
// task 1002909; greenfield is exempt; a full box → 409, ../capacity-gate.js)
|
|
21
21
|
// GET /provisioning/instances — list the caller's instances
|
|
22
22
|
// GET /provisioning/instances/:id — read one of the caller's instances
|
|
23
23
|
// (also SELF-HEALS its catalog projection,
|
|
@@ -51,6 +51,7 @@ const { ALWAYS_ON_CORE, OPTIONAL_MODULES, starterBundleAnswerTable, optionalModu
|
|
|
51
51
|
const { recommendationsForDetails } = require('../recommendations');
|
|
52
52
|
const { makeAskRateLimiter, clientIp } = require('../rate-limit');
|
|
53
53
|
const { requireAuthorityForMeteredShape } = require('../paid-shape-gate');
|
|
54
|
+
const { requireRoomOnBox } = require('../capacity-gate'); // a full box refused up front (task 1003938)
|
|
54
55
|
const { adoptCredentialFault } = require('../credential-preflight');
|
|
55
56
|
const catalogBridge = require('../catalog-bridge');
|
|
56
57
|
const capacity = require('../capacity'); // the box's slot ceiling (ADR 0285, task 1003927)
|
|
@@ -299,10 +300,9 @@ module.exports = function provisioningRoutes() {
|
|
|
299
300
|
});
|
|
300
301
|
|
|
301
302
|
// POST /provisioning/instances — request a new instance. rank: any authenticated
|
|
302
|
-
// builder (own resource); a `dedicated` (metered) shape
|
|
303
|
-
//
|
|
304
|
-
// ENQUEUES a 'provision' intent
|
|
305
|
-
// the runner (task P3) does the real standup. Idempotent on slug for the SAME
|
|
303
|
+
// builder (own resource); a `dedicated` (metered) shape also needs provisioning.fleet.manage
|
|
304
|
+
// (../paid-shape-gate.js), and a full shared box answers 409 box_full up front (../capacity-gate.js).
|
|
305
|
+
// ENQUEUES a 'provision' intent the runner (task P3) stands up. Idempotent on slug for the SAME
|
|
306
306
|
// owner (re-request returns the existing row); a slug owned by ANOTHER builder → 409.
|
|
307
307
|
// Body: { slug (required), target_ref?, hosting_shape? (co-tenant|dedicated),
|
|
308
308
|
// domain?, tier?, onboard_mode? (greenfield|adopt) }.
|
|
@@ -312,7 +312,7 @@ module.exports = function provisioningRoutes() {
|
|
|
312
312
|
// ALREADY has, so the runner must clone + layer onto its history instead of pushing a
|
|
313
313
|
// fresh tree at `main` (which an existing repo rejects as unrelated history). Absent →
|
|
314
314
|
// 'greenfield', the safe default.
|
|
315
|
-
router.post('/provisioning/instances', auth.requireBuilder, requireAuthorityForMeteredShape(auth.requirePermission), async (req, res) => {
|
|
315
|
+
router.post('/provisioning/instances', auth.requireBuilder, requireAuthorityForMeteredShape(auth.requirePermission), requireRoomOnBox(pool, log), async (req, res) => {
|
|
316
316
|
if (validateOrRespond(req, res, {
|
|
317
317
|
slug: { required: true, type: 'string', maxLength: 40 },
|
|
318
318
|
target_ref: { type: 'string', maxLength: 200 },
|
|
@@ -939,7 +939,21 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
939
939
|
than letting the word "Delete" imply it. -->
|
|
940
940
|
<details class="finePrint" id="mDelWhat">
|
|
941
941
|
<summary>What this removes — and what it keeps</summary>
|
|
942
|
-
<p>Tearing down stops the project’s server and its nightly backups. It does <b>not</b> erase the project: it stays in your list marked <b>torn down</b> so you can bring it back, your repository on GitHub is untouched, and its database and existing backups are kept on purpose.
|
|
942
|
+
<p>Tearing down stops the project’s server and its nightly backups. It does <b>not</b> erase the project: it stays in your list marked <b>torn down</b> so you can bring it back, your repository on GitHub is untouched, and its database and existing backups are kept on purpose. To erase it for good — its database, its backups and its web address — use <b>Disconnect from Bongos entirely</b> below.</p>
|
|
943
|
+
</details>
|
|
944
|
+
</div>
|
|
945
|
+
<!-- "Disconnect from Bongos entirely" (task 1003249, ADR 0290): the owner's TRUE
|
|
946
|
+
delete, in its own card after the ender so the reversible control never reads
|
|
947
|
+
as the destructive one. The fold is a sibling of the volatile slot, like
|
|
948
|
+
#mDelWhat, so no re-render can drop what it promises. -->
|
|
949
|
+
<div class="card mSec" id="mDisconnect">
|
|
950
|
+
<h2>Disconnect from Bongos entirely</h2>
|
|
951
|
+
<div id="mDisc"><p class="finePrint">Reading the project…</p></div>
|
|
952
|
+
<details class="finePrint" id="mDiscWhat">
|
|
953
|
+
<summary>What this erases — and what it keeps</summary>
|
|
954
|
+
<p><b>Erased for good:</b> the project’s server, its database and every nightly backup of it, its settings, its place on the map and the list of its members. Its name and web address are released, so another project can take them.</p>
|
|
955
|
+
<p><b>Kept:</b> your repository on GitHub, exactly as it is. If a GitHub sign-in app was registered for the project, that stays on your GitHub account too — remove it in your GitHub settings if you no longer want it.</p>
|
|
956
|
+
<p>This cannot be undone. To stop the project and keep everything, use <b>Take it offline</b> above instead.</p>
|
|
943
957
|
</details>
|
|
944
958
|
</div>
|
|
945
959
|
</section>
|
|
@@ -5196,21 +5210,19 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5196
5210
|
retention is deliberate on shared boxes; a dedicated machine takes its
|
|
5197
5211
|
database with it). Copy follows the runner, never the other way around. */
|
|
5198
5212
|
function teardownTruth(inst) {
|
|
5199
|
-
/* the address line is NOT decoration
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
both the honest word and the accurate one, since a revive re-uses exactly
|
|
5205
|
-
this hostname. */
|
|
5213
|
+
/* the address line is NOT decoration. Teardown now parks the hostname at a
|
|
5214
|
+
dead address and removes its proxy block (task 1002897), so nothing answers
|
|
5215
|
+
there — but the row keeps the address, because a revive re-uses exactly
|
|
5216
|
+
this hostname. "Reserved" is the accurate word for that; "gone" would not be,
|
|
5217
|
+
and neither is the old error-page promise, true only before the park. */
|
|
5206
5218
|
/* pre-standup: at 'requested' nothing has been built, so the shape arms
|
|
5207
5219
|
below would describe a server and a database that never existed. error
|
|
5208
5220
|
and provisioning keep the built-world text on purpose — a run that failed
|
|
5209
5221
|
partway may already have a database, a port and a DNS record. */
|
|
5210
5222
|
if (inst && inst.status === 'requested') return 'Nothing has been stood up yet — this cancels the standup. Anything already on GitHub is untouched, and the name and web address stay reserved for this project.';
|
|
5211
5223
|
return inst && inst.hosting_shape === 'dedicated'
|
|
5212
|
-
? 'This project has a machine to itself, so the machine goes away and its database goes with it. Your repository is untouched. Its web address stays reserved for this project and
|
|
5213
|
-
: 'The server goes away. Your repository is untouched. The database is kept, so bringing it back later restores its memory. Its web address stays reserved for this project and
|
|
5224
|
+
? 'This project has a machine to itself, so the machine goes away and its database goes with it. Your repository is untouched. Its web address stays reserved for this project, and nothing answers there until you bring it back. Taking it offline erases nothing.'
|
|
5225
|
+
: 'The server goes away. Your repository is untouched. The database is kept, so bringing it back later restores its memory. Its web address stays reserved for this project, and nothing answers there until you bring it back. Taking it offline erases nothing.';
|
|
5214
5226
|
}
|
|
5215
5227
|
|
|
5216
5228
|
/* onManage: the same facts, but the pointers name the controls THIS surface
|
|
@@ -5224,7 +5236,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5224
5236
|
"nothing more will happen" (task 1003232) */
|
|
5225
5237
|
if (inst.status === 'torn_down' || inst.status === 'tearing_down') {
|
|
5226
5238
|
if (!inst.domain) return '';
|
|
5227
|
-
return 'Its address, <code>' + esc(inst.domain) + '</code>, stays reserved for this project —
|
|
5239
|
+
return 'Its address, <code>' + esc(inst.domain) + '</code>, stays reserved for this project — nothing answers there until you bring it back.';
|
|
5228
5240
|
}
|
|
5229
5241
|
if (inst.domain) {
|
|
5230
5242
|
if (inst.status === 'active') {
|
|
@@ -5553,6 +5565,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5553
5565
|
it: the delete section reads only status + open_intent, both already in
|
|
5554
5566
|
hand, and a queued teardown must show up without a reload */
|
|
5555
5567
|
renderDelete(inst);
|
|
5568
|
+
renderDisconnect(inst);
|
|
5556
5569
|
/* its address (task 1003355) is fetch-free too — the control it carries
|
|
5557
5570
|
reads status + domain + open_intent, all already in hand */
|
|
5558
5571
|
renderAddress(inst);
|
|
@@ -7026,6 +7039,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7026
7039
|
'<p class="finePrint">This page updates when it starts.</p>');
|
|
7027
7040
|
return;
|
|
7028
7041
|
}
|
|
7042
|
+
/* a queued disconnect takes the project offline itself; a teardown ask here would
|
|
7043
|
+
only be refused by the server's open-run check (task 1003249) */
|
|
7044
|
+
if (inst.open_intent && inst.open_intent.action === 'disconnect') {
|
|
7045
|
+
delMsg('<p class="finePrint">A disconnect is queued for this project — taking it offline is part of it.</p>');
|
|
7046
|
+
return;
|
|
7047
|
+
}
|
|
7029
7048
|
/* a project that never finished standing up still reaches here — that is
|
|
7030
7049
|
the whole point of the widened gate; teardownTruth() scopes the copy to
|
|
7031
7050
|
its status */
|
|
@@ -7116,6 +7135,92 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7116
7135
|
if (t.hasAttribute('data-del-no')) { mDelAsking = null; if (manageInst) renderDelete(manageInst); }
|
|
7117
7136
|
});
|
|
7118
7137
|
|
|
7138
|
+
/* ── "Disconnect from Bongos entirely" (task 1003249, ADR 0290): the TRUE delete. The
|
|
7139
|
+
ender's shape on purpose — the name typed back (and checked again by the SERVER, which
|
|
7140
|
+
receives it as confirm_slug), a project-scoped guard so the minute tick never repaints
|
|
7141
|
+
over a half-typed name, and every refusal in the server's own words. ── */
|
|
7142
|
+
var mDiscAsking = null;
|
|
7143
|
+
|
|
7144
|
+
function discMsg(html) { $('mDisc').innerHTML = html; }
|
|
7145
|
+
|
|
7146
|
+
function renderDisconnect(inst) {
|
|
7147
|
+
if (mDiscAsking && mDiscAsking === inst.slug) return;
|
|
7148
|
+
var open = inst.open_intent;
|
|
7149
|
+
if (open && open.action === 'disconnect') {
|
|
7150
|
+
discMsg('<div class="addr">This project is being disconnected.</div>' +
|
|
7151
|
+
'<p class="finePrint">When it finishes, the project leaves your list and this page can no longer find it.</p>');
|
|
7152
|
+
return;
|
|
7153
|
+
}
|
|
7154
|
+
if (open) {
|
|
7155
|
+
discMsg('<p class="plain">Disconnecting waits for the current ' + esc(open.action || '') + ' run to finish.</p>');
|
|
7156
|
+
return;
|
|
7157
|
+
}
|
|
7158
|
+
discMsg('<p class="plain">Erase this project from the platform for good: its database and backups, its settings and its place on the map. Your repository on GitHub is kept.</p>' +
|
|
7159
|
+
'<div class="actions"><button type="button" class="btn small danger" data-disc-ask>Disconnect entirely</button></div>');
|
|
7160
|
+
}
|
|
7161
|
+
|
|
7162
|
+
function askDisconnect() {
|
|
7163
|
+
var inst = manageInst;
|
|
7164
|
+
if (!inst) return;
|
|
7165
|
+
mDiscAsking = inst.slug;
|
|
7166
|
+
discMsg('<div class="confirmRow"><span class="plain">Disconnect <b>' + esc(inst.slug) + '</b> from Bongos entirely? ' +
|
|
7167
|
+
'Its database and backups are erased and this cannot be undone. Type the project’s name to confirm.</span></div>' +
|
|
7168
|
+
'<div class="attach">' +
|
|
7169
|
+
'<input type="text" id="mDiscName" autocomplete="off" spellcheck="false" maxlength="40" ' +
|
|
7170
|
+
'aria-label="Type the project name to confirm the disconnect" placeholder="' + esc(inst.slug) + '">' +
|
|
7171
|
+
'<button type="button" class="btn small danger" data-disc-yes disabled>Yes, disconnect it</button>' +
|
|
7172
|
+
'<button type="button" class="linklike" data-disc-no>Keep it</button></div>');
|
|
7173
|
+
var field = $('mDiscName');
|
|
7174
|
+
if (field) field.focus();
|
|
7175
|
+
}
|
|
7176
|
+
|
|
7177
|
+
function discTypedMatches() {
|
|
7178
|
+
var field = $('mDiscName');
|
|
7179
|
+
return !!(field && manageInst && field.value.trim() === manageInst.slug);
|
|
7180
|
+
}
|
|
7181
|
+
|
|
7182
|
+
function doDisconnect() {
|
|
7183
|
+
var inst = manageInst;
|
|
7184
|
+
if (!inst || !discTypedMatches()) return;
|
|
7185
|
+
var typed = $('mDiscName').value.trim();
|
|
7186
|
+
mDiscAsking = null;
|
|
7187
|
+
discMsg('<p class="finePrint">Asking…</p>');
|
|
7188
|
+
fetch(API + '/provisioning/instances/' + encodeURIComponent(inst.id) + '/disconnect', {
|
|
7189
|
+
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
|
7190
|
+
body: JSON.stringify({ confirm_slug: typed })
|
|
7191
|
+
}).then(function (res) {
|
|
7192
|
+
return jsonOrEmpty(res).then(function (data) {
|
|
7193
|
+
if (res.ok) {
|
|
7194
|
+
discMsg('<div class="projMsg ok">' + esc(data.message || 'Disconnect requested.') + '</div>');
|
|
7195
|
+
loadManage(true); /* the queued run this page renders just changed */
|
|
7196
|
+
return;
|
|
7197
|
+
}
|
|
7198
|
+
renderDisconnect(inst);
|
|
7199
|
+
var e = apiError(data, res.status);
|
|
7200
|
+
$('mDisc').insertAdjacentHTML('beforeend', '<div class="projMsg err">' +
|
|
7201
|
+
(res.status === 401 ? signInAgain() : esc(e.message || 'The disconnect could not be queued — try again.')) + '</div>');
|
|
7202
|
+
});
|
|
7203
|
+
}).catch(function () {
|
|
7204
|
+
renderDisconnect(inst);
|
|
7205
|
+
$('mDisc').insertAdjacentHTML('beforeend',
|
|
7206
|
+
'<div class="projMsg err">Couldn’t reach the platform — check your connection and try again.</div>');
|
|
7207
|
+
});
|
|
7208
|
+
}
|
|
7209
|
+
|
|
7210
|
+
$('mDisc').addEventListener('input', function (ev) {
|
|
7211
|
+
if (!ev.target || ev.target.id !== 'mDiscName') return;
|
|
7212
|
+
var yes = $('mDisc').querySelector('[data-disc-yes]');
|
|
7213
|
+
if (yes) yes.disabled = !discTypedMatches();
|
|
7214
|
+
});
|
|
7215
|
+
|
|
7216
|
+
$('mDisc').addEventListener('click', function (ev) {
|
|
7217
|
+
var t = ev.target.closest && ev.target.closest('button');
|
|
7218
|
+
if (!t) return;
|
|
7219
|
+
if (t.hasAttribute('data-disc-ask')) return askDisconnect();
|
|
7220
|
+
if (t.hasAttribute('data-disc-yes')) return doDisconnect();
|
|
7221
|
+
if (t.hasAttribute('data-disc-no')) { mDiscAsking = null; if (manageInst) renderDisconnect(manageInst); }
|
|
7222
|
+
});
|
|
7223
|
+
|
|
7119
7224
|
/* ── restart an unreachable project (task 1003238). Its own route, because
|
|
7120
7225
|
PROVISION_ENQUEUE_FROM excludes 'active' — re-POSTing the slug the way
|
|
7121
7226
|
Try again / Bring it back do would be refused on every press. Every
|
|
@@ -155,6 +155,9 @@
|
|
|
155
155
|
{ "name": "manage flows: the ender asks for the name typed back, queues the teardown, and the page re-reads the project as queued", "auth": true, "width": 1440, "url": "/projects?view=manage&id=105",
|
|
156
156
|
"steps": [["wait",900],["click","[data-del-ask]"],["wait",200],["fill","#mDelName","mooring"],["wait",100],["click","[data-del-yes]"],["wait",900]],
|
|
157
157
|
"expect": {"text":[["#mDel .addr","A teardown is queued for this project."],["#mName","mooring"]],"hidden":["#mDelName","[data-del-ask]"],"visible":["#mStrip .strip.flowDown"]} },
|
|
158
|
+
{ "name": "manage flows: disconnect entirely asks for the name typed back, queues the true delete, and the page re-reads the project as being disconnected (task 1003249)", "auth": true, "width": 1440, "url": "/projects?view=manage&id=104",
|
|
159
|
+
"steps": [["wait",900],["click","[data-disc-ask]"],["wait",200],["fill","#mDiscName","old-lantern"],["wait",100],["click","[data-disc-yes]"],["wait",900]],
|
|
160
|
+
"expect": {"text":[["#mDisc .addr","This project is being disconnected."],["#mName","old-lantern"]],"hidden":["#mDiscName","[data-disc-ask]"]} },
|
|
158
161
|
{ "name": "the wizard driver (task 1003346): name, type, about, home (fresh; the stub grants repo access, so the new repository is named from the project), address (the handle's verdict), the review as a ledger, then the filing — the done panel with the standup rail and the human steps", "auth": true, "width": 1440, "url": "/projects?view=new",
|
|
159
162
|
"steps": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#doCreate"],["wait",1500]],
|
|
160
163
|
"expect": {"visible":["#panel-done","#liveProgress .prog","#liveProgress .prog .stages li.now","#humanSteps li"],"hidden":["#panel-review","#panel-name"],"text":[["#done-h","The drums are rolling"]],"attr":[["#panel-done","class","panel on"]]} },
|
|
@@ -157,6 +157,7 @@ function stamp(v) {
|
|
|
157
157
|
const FLOW_RECEIPTS = {
|
|
158
158
|
teardown: { queued: true, message: 'Teardown queued.' },
|
|
159
159
|
restart: { queued: true, message: 'Restart queued.' },
|
|
160
|
+
disconnect: { queued: true, message: 'Queued. The platform takes the project down, erases its database and backups, and releases its name and address. It leaves your list when that is done.' },
|
|
160
161
|
domain: { recorded: true, queued: true, message: 'Address recorded.' },
|
|
161
162
|
// the wizard's filing (task 1003346): the receipt names the instance the page then polls — Harbor, the fixture's standing-up row — and the address the page promises is the one the server assigned
|
|
162
163
|
provision: { queued: true, message: 'Queued. The platform picks it up from here.', instance: { id: 102, domain: 'mercury.cloudbongos.com' } },
|
|
@@ -220,10 +221,10 @@ function withPlanet(inst) {
|
|
|
220
221
|
}
|
|
221
222
|
function flowReceipt(rest, method) {
|
|
222
223
|
if (method === 'POST' && rest === '/provisioning/instances') return FLOW_RECEIPTS.provision;
|
|
223
|
-
const m = rest.match(/^\/provisioning\/instances\/(\d+)\/(teardown|restart|domain)$/);
|
|
224
|
+
const m = rest.match(/^\/provisioning\/instances\/(\d+)\/(teardown|restart|domain|disconnect)$/);
|
|
224
225
|
if (!m) return null;
|
|
225
226
|
if (method !== 'POST') return null;
|
|
226
|
-
if (m[2] === 'teardown' || m[2] === 'restart') ASKED.set(m[1], m[2]);
|
|
227
|
+
if (m[2] === 'teardown' || m[2] === 'restart' || m[2] === 'disconnect') ASKED.set(m[1], m[2]);
|
|
227
228
|
return FLOW_RECEIPTS[m[2]];
|
|
228
229
|
}
|
|
229
230
|
function withAsk(inst) {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"default": true,
|
|
7
7
|
"maintenance": { "status": "core-maintained" },
|
|
8
8
|
"contributes": { "skills": ["design", "design-sync", "figma-design-sync", "design-taste-frontend", "design-taste-frontend-v1", "high-end-visual-design", "redesign-existing-projects", "impeccable", "style", "minimalist-ui", "industrial-brutalist-ui", "gpt-taste", "stitch-design-taste", "image-to-code", "imagegen-frontend-web", "imagegen-frontend-mobile", "brandkit"], "disciplines": ["ui"] },
|
|
9
|
+
"declarativeSeams": { "skills": ["design", "design-sync", "figma-design-sync"] },
|
|
9
10
|
"provides": [],
|
|
10
11
|
"consumes": []
|
|
11
12
|
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.774",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.774",
|
|
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.19.
|
|
3
|
+
"version": "1.19.774",
|
|
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",
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/gds/address-park-sweep.sh — park the addresses of projects torn down BEFORE
|
|
3
|
+
# teardown learned to do it itself (task 1003947, the backfill half of task 1002897).
|
|
4
|
+
#
|
|
5
|
+
# WHY. Task 1002897 made a teardown remove its <slug>.caddy block and park its hostname
|
|
6
|
+
# at the RFC 5737 blackhole. That only applies to teardowns run after the pin carries it,
|
|
7
|
+
# so every earlier row still has its Caddy block proxying to a dead port and its A record
|
|
8
|
+
# pointing at the platform: eleven hostnames answered 502 on 2026-09-16, while the hub
|
|
9
|
+
# copy says "nothing answers there". This is that same teardown step, run once over the
|
|
10
|
+
# ledger's torn-down rows. It does not re-run `provision.js teardown <slug>`, which would
|
|
11
|
+
# rewrite each row's torn_down_at.
|
|
12
|
+
#
|
|
13
|
+
# The reserved-hostname rule, the zone and the apex come from the INSTALLED core (the
|
|
14
|
+
# same helpers a teardown calls), so this sweep cannot disagree with what teardown parks.
|
|
15
|
+
#
|
|
16
|
+
# Run on the control plane as a sudoer:
|
|
17
|
+
# bash scripts/gds/address-park-sweep.sh # survey only — read-only
|
|
18
|
+
# bash scripts/gds/address-park-sweep.sh --apply # remove the snippets, reload Caddy, park the hostnames
|
|
19
|
+
#
|
|
20
|
+
# Left in place on purpose, even with --apply: each project's database, dumps and checkout,
|
|
21
|
+
# and the row's domain + port (ADR 0181 — a revive re-uses that address).
|
|
22
|
+
set -uo pipefail
|
|
23
|
+
|
|
24
|
+
APPLY=0; [ "${1:-}" = "--apply" ] && APPLY=1
|
|
25
|
+
DB="${PGDATABASE:-cloudbongos}"
|
|
26
|
+
INSTANCE_DIR="${INSTANCE_DIR:-/home/lars/cloudbongos-instance}"
|
|
27
|
+
SITES="${PROVISION_CADDY_SNIPPET_DIR:-/etc/caddy/sites}"
|
|
28
|
+
BOX_ENV="${BOX_ENV:-/etc/cloudbongos/box.env}"
|
|
29
|
+
|
|
30
|
+
rows=$(sudo -n -u lars psql -d "$DB" -tAF '|' -c "select slug, coalesce(domain, '') from provisioning_instances where status='torn_down' order by slug" 2>/dev/null) \
|
|
31
|
+
|| { echo "cannot read provisioning_instances from $DB as lars"; exit 1; }
|
|
32
|
+
|
|
33
|
+
# slug|domain|zone|reserved|blackhole per row, plus an apex line — from the installed core.
|
|
34
|
+
plan=$(sudo -n -u lars env ROWS="$rows" bash -c 'cd "$1" && exec node -' _ "$INSTANCE_DIR" <<'JS'
|
|
35
|
+
const core = './node_modules/@bongos/core';
|
|
36
|
+
const { isReservedHostname } = require(`${core}/modules/provisioning/provisioning.js`);
|
|
37
|
+
const { dnsReleaseEnv } = require(`${core}/scripts/gds/provision-net.js`);
|
|
38
|
+
const apex = require(`${core}/scripts/gds/instance-apex.js`).instanceApex();
|
|
39
|
+
console.log(`apex|${apex || ''}`);
|
|
40
|
+
for (const line of String(process.env.ROWS || '').split('\n').filter(Boolean)) {
|
|
41
|
+
const [slug, domain] = line.split('|');
|
|
42
|
+
const env = domain ? dnsReleaseEnv(domain) : { CLOUDFLARE_ZONE: '', BOX_IP: '' };
|
|
43
|
+
console.log(['row', slug, domain || '', env.CLOUDFLARE_ZONE, domain && isReservedHostname(domain, apex) ? 1 : 0, env.BOX_IP].join('|'));
|
|
44
|
+
}
|
|
45
|
+
JS
|
|
46
|
+
) || { echo "cannot load the installed core from $INSTANCE_DIR"; exit 1; }
|
|
47
|
+
|
|
48
|
+
apex=$(printf '%s\n' "$plan" | awk -F'|' '$1 == "apex" {print $2}')
|
|
49
|
+
[ -n "$apex" ] || { echo "the installed core names no apex — refusing: the reserved-hostname guard would be blind"; exit 1; }
|
|
50
|
+
echo "== torn-down rows still holding a Caddy block or an unparked address (apex $apex) =="
|
|
51
|
+
|
|
52
|
+
left=(); reload=0
|
|
53
|
+
while IFS='|' read -r kind slug domain zone reserved blackhole; do
|
|
54
|
+
[ "$kind" = "row" ] || continue
|
|
55
|
+
# The slug grammar provisioning.isValidSlug enforces; anything else never reaches a command.
|
|
56
|
+
[[ "$slug" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]] || continue
|
|
57
|
+
if [ -e "$SITES/$slug.caddy" ]; then
|
|
58
|
+
printf ' %-22s snippet %s\n' "$slug" "$SITES/$slug.caddy"
|
|
59
|
+
left+=("$slug:snippet")
|
|
60
|
+
if [ "$APPLY" = 1 ]; then sudo -n rm -f "$SITES/$slug.caddy" </dev/null && reload=1; fi
|
|
61
|
+
fi
|
|
62
|
+
[ -n "$domain" ] || continue
|
|
63
|
+
if [ "$reserved" = 1 ]; then printf ' %-22s %s is platform-reserved — never parked\n' "$slug" "$domain"; continue; fi
|
|
64
|
+
now=$(getent ahostsv4 "$domain" 2>/dev/null </dev/null | awk 'NR == 1 {print $1}')
|
|
65
|
+
[ -z "$now" ] || [ "$now" = "$blackhole" ] && continue
|
|
66
|
+
printf ' %-22s %s resolves to %s — park at %s (zone %s)\n' "$slug" "$domain" "$now" "$blackhole" "$zone"
|
|
67
|
+
left+=("$slug:dns")
|
|
68
|
+
[ "$APPLY" = 1 ] || continue
|
|
69
|
+
sudo -n -u lars bash -c 'set -a; . "$1"; set +a; BOX_HOSTNAME="$2" BOX_IP="$3" CLOUDFLARE_ZONE="$4" BOX_PROXIED=0 exec "$BOX_DNS_HOOK"' \
|
|
70
|
+
_ "$BOX_ENV" "$domain" "$blackhole" "$zone" </dev/null || echo " ⚠ could not park $domain — re-run to retry"
|
|
71
|
+
done <<< "$plan"
|
|
72
|
+
|
|
73
|
+
[ "$reload" = 1 ] && { sudo -n systemctl reload caddy || echo " ⚠ caddy reload failed — the removed blocks are still loaded"; }
|
|
74
|
+
|
|
75
|
+
echo "== summary =="
|
|
76
|
+
if [ "$APPLY" = 1 ]; then echo " acted on (${#left[@]}): ${left[*]:-none}"; else echo " left (${#left[@]}): ${left[*]:-none}"; fi
|
|
77
|
+
if [ "$APPLY" = 1 ]; then echo " (applied — re-run without --apply to confirm nothing is left; DNS may take a TTL to settle)";
|
|
78
|
+
else echo " (survey only — re-run with --apply to remove the snippets, reload Caddy and park the hostnames)"; fi
|
package/scripts/gds/cli-lib.js
CHANGED
|
@@ -1166,6 +1166,42 @@ function detectWorktreeName() {
|
|
|
1166
1166
|
}
|
|
1167
1167
|
}
|
|
1168
1168
|
|
|
1169
|
+
// ---- card richness (task 1002518) ------------------------------------------
|
|
1170
|
+
//
|
|
1171
|
+
// The one place a CLI turns a /me payload into "what should this card look
|
|
1172
|
+
// like": three booleans — print the card at all, write the widget, open its
|
|
1173
|
+
// summary — that emitCard consumes directly.
|
|
1174
|
+
//
|
|
1175
|
+
// WHY THIS READS THE PAYLOAD AND NEVER require()s render-prefs.js. The ladder
|
|
1176
|
+
// lives in modules/builder-settings/render-prefs.js, and requiring it from here
|
|
1177
|
+
// would work on a full checkout and BREAK on a task-scoped dev box, which
|
|
1178
|
+
// carries scripts/ and src/bongos/ but not every modules/ directory (task
|
|
1179
|
+
// 1003098 — tree-preflight exists precisely to fail an entry point that reaches
|
|
1180
|
+
// for module code, and start.js is one). So the server resolves the rung and
|
|
1181
|
+
// ships the answers on `render.flags`; this function only picks them up.
|
|
1182
|
+
//
|
|
1183
|
+
// TWO SERVER SHAPES, deliberately. `render.flags` is the ladder (task 1002518);
|
|
1184
|
+
// `render.widgets_enabled` is the boolean that predates it. A CLI newer than the
|
|
1185
|
+
// instance it is pointed at sees only the boolean — the /me payload is not
|
|
1186
|
+
// version-negotiated — so the fallback is not defensive padding, it is the
|
|
1187
|
+
// supported path against a pinned instance, and it reconstructs exactly what
|
|
1188
|
+
// that boolean has always meant: card always, widget iff true, never expanded.
|
|
1189
|
+
// An absent or failed /me lands on the same default.
|
|
1190
|
+
function cardFlagsFromMe(me) {
|
|
1191
|
+
const render = me && me.data && me.data.render;
|
|
1192
|
+
const f = render && render.flags;
|
|
1193
|
+
if (f && typeof f === 'object') {
|
|
1194
|
+
return {
|
|
1195
|
+
level: typeof f.level === 'string' ? f.level : null,
|
|
1196
|
+
showCard: f.showCard !== false,
|
|
1197
|
+
showWidget: f.showWidget === true,
|
|
1198
|
+
expanded: f.expanded === true,
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
const enabled = !render || render.widgets_enabled !== false;
|
|
1202
|
+
return { level: null, showCard: true, showWidget: enabled, expanded: false };
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1169
1205
|
module.exports = {
|
|
1170
1206
|
API_BASE,
|
|
1171
1207
|
detectWorktreeName,
|
|
@@ -1220,4 +1256,5 @@ module.exports = {
|
|
|
1220
1256
|
printOnboardingNudge,
|
|
1221
1257
|
printCostWarning,
|
|
1222
1258
|
printNeedsNudge,
|
|
1259
|
+
cardFlagsFromMe,
|
|
1223
1260
|
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// scripts/gds/fitness-migration-contraction.js — the ONE shape of destructive DDL a
|
|
2
|
+
// module migration may carry: an ACKNOWLEDGED contract-phase DROP COLUMN on its own
|
|
3
|
+
// namespaced table (task 1003092; ADR 0083 §Module-owned migrations, amended; the
|
|
4
|
+
// expand-then-contract pattern of ADR 0177).
|
|
5
|
+
//
|
|
6
|
+
// WHY. Check 11 in fitness.js keeps module migrations ADDITIVE: there is no down
|
|
7
|
+
// path, "disable a module" means "stop applying", so a DROP can only be an accident.
|
|
8
|
+
// That rule met its first legitimate exception the moment a module ran an
|
|
9
|
+
// expand-then-contract rename (ADR 0177 §"the fifth use"): `platform_identity_010`
|
|
10
|
+
// ADDed `membership_kind` beside the legacy `role` column and kept the pair in
|
|
11
|
+
// lockstep; the CONTRACT phase has to DROP the legacy column, or the rename never
|
|
12
|
+
// finishes and the shim outlives its release (ADR 0174's Lineage lesson). A drop
|
|
13
|
+
// that a previous migration wrote down in advance is not an accident.
|
|
14
|
+
//
|
|
15
|
+
// THE SHAPE, AND WHY IT IS THIS NARROW. A statement is let through only when ALL of:
|
|
16
|
+
// 1. the FILE carries the `-- forward-unsafe: <reason>` pragma — the same
|
|
17
|
+
// acknowledgement scripts/gds/lint-migrations.js requires of a core migration,
|
|
18
|
+
// so one vocabulary says "this destruction is deliberate" everywhere;
|
|
19
|
+
// 2. the statement is EXACTLY `ALTER TABLE <t> DROP COLUMN [IF EXISTS] <c>` and
|
|
20
|
+
// nothing else — no second clause, no DROP TABLE, no TRUNCATE, no bare DROP
|
|
21
|
+
// CONSTRAINT (which stays a violation even with the pragma);
|
|
22
|
+
// 3. <t> is inside the module's own namespace (or its grandfathered legacy set),
|
|
23
|
+
// the same test the namespace check applies to every ALTER.
|
|
24
|
+
// The pragma does not turn the check off for the file: every OTHER destructive
|
|
25
|
+
// statement in an acknowledged file is still flagged. PURE — no fs, no process.
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
// Same single-line acknowledgement lint-migrations.js captures (ACK_RE there).
|
|
29
|
+
const ACK_RE = /^\s*--\s*forward-unsafe\s*:\s*(.+)$/im;
|
|
30
|
+
|
|
31
|
+
// Exactly one column drop, on one table, and nothing else in the statement.
|
|
32
|
+
const COLUMN_DROP_RE = /^\s*alter\s+table\s+(?:if\s+exists\s+)?(?:only\s+)?("?[\w.]+"?)\s+drop\s+column\s+(?:if\s+exists\s+)?"?\w+"?\s*$/i;
|
|
33
|
+
|
|
34
|
+
// Is the migration FILE acknowledged as forward-unsafe? Read on the RAW sql — the
|
|
35
|
+
// pragma is a comment, and the destructive-DDL scan strips comments first.
|
|
36
|
+
function acknowledgedContraction(rawSql) {
|
|
37
|
+
const m = ACK_RE.exec(String(rawSql == null ? '' : rawSql));
|
|
38
|
+
return m ? m[1].trim() : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Is THIS statement the one allowed shape? `inNamespace(table)` is the caller's
|
|
42
|
+
// namespace predicate (fitness.js owns the <key>_ / legacy-set rule).
|
|
43
|
+
function isAcknowledgedColumnContraction(stmt, { acknowledged, inNamespace }) {
|
|
44
|
+
if (!acknowledged) return false;
|
|
45
|
+
const m = COLUMN_DROP_RE.exec(stmt);
|
|
46
|
+
if (!m) return false;
|
|
47
|
+
return inNamespace(m[1]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { acknowledgedContraction, isAcknowledgedColumnContraction };
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -827,6 +827,12 @@ function isConstraintWidening(stmt, reAdded) {
|
|
|
827
827
|
return reAdded.has(m[1].replace(/"/g, '')); // re-added same-named constraint → widening
|
|
828
828
|
}
|
|
829
829
|
|
|
830
|
+
// The ONE destructive shape a module may carry: an ACKNOWLEDGED contract-phase
|
|
831
|
+
// `ALTER TABLE <own table> DROP COLUMN <c>` (task 1003092 — the expand-then-contract
|
|
832
|
+
// rename of ADR 0177 has to finish; ADR 0083 §Module-owned migrations, amended). The
|
|
833
|
+
// pragma is read off the RAW sql (it is a comment); everything else here still runs.
|
|
834
|
+
const { acknowledgedContraction, isAcknowledgedColumnContraction } = require('./fitness-migration-contraction.js');
|
|
835
|
+
|
|
830
836
|
// Pure: violations for ONE module's migration SQL. Exported for the negative test.
|
|
831
837
|
function moduleMigrationViolations(key, sql) {
|
|
832
838
|
const ns = moduleNamespace(key);
|
|
@@ -835,8 +841,11 @@ function moduleMigrationViolations(key, sql) {
|
|
|
835
841
|
const violations = [];
|
|
836
842
|
// additive — scan per statement so an ALTER…DROP never spans a `;`
|
|
837
843
|
const reAdded = reAddedConstraintNames(clean);
|
|
844
|
+
const acknowledged = acknowledgedContraction(sql);
|
|
845
|
+
const ownTable = (t) => inNamespace(t, ns, legacy);
|
|
838
846
|
for (const stmt of clean.split(';')) {
|
|
839
847
|
if (isConstraintWidening(stmt, reAdded)) continue; // additive CHECK-enum widening (task 1978)
|
|
848
|
+
if (isAcknowledgedColumnContraction(stmt, { acknowledged, inNamespace: ownTable })) continue; // contract phase (task 1003092)
|
|
840
849
|
for (const d of DESTRUCTIVE_DDL) {
|
|
841
850
|
if (d.re.test(stmt)) {
|
|
842
851
|
violations.push(`module "${key}" migration uses destructive DDL (${d.label}) — module migrations must be ADDITIVE; disabling a module stops applying, it never drops (ADR 0083 §Decision #5).`);
|
|
@@ -1205,6 +1214,15 @@ const CHECKS = [
|
|
|
1205
1214
|
// migrations: same shared, monotonic, human-allocated number line, and until this
|
|
1206
1215
|
// landed only one of the two was enforced. An ADR number is a citation target, so a
|
|
1207
1216
|
// duplicate is wrong forever (ADR 0195).
|
|
1217
|
+
// Check 33 — task 1003819: a module declares what it contributes, and nothing
|
|
1218
|
+
// checked that a declared seam is actually WIRED. verifyPortsSatisfied asks the
|
|
1219
|
+
// opposite question (every `consumes` has a provider) and validateManifest checks
|
|
1220
|
+
// only the SHAPE, so a contributes entry whose artifact was never shipped — or was
|
|
1221
|
+
// renamed out from under the manifest — boots silent: every consumer skips it the
|
|
1222
|
+
// same way it correctly skips a real declarative-only entry. CI, not boot: nothing
|
|
1223
|
+
// crashes later, so a start-up refusal would trade an outage risk for a cosmetic
|
|
1224
|
+
// one (ADR 0289). Rationale + the seam→consumer table live in that file.
|
|
1225
|
+
require('./seam-wiring-guard.js').checkSeamWiring,
|
|
1208
1226
|
require('./adr-namespace.js').checkAdrNumbering, require('./exec-path-guard.js').checkExecPathsStatic, // Check 32 — task 1003371 / B50: what the server EXECUTES, out of whose tree — the blind spot all three 2026-08-29 CRITICALs shared. Rationale + reviewed exemptions live in that file. Sharing Check 31's line (the 19c/19d + 23/24 precedent above): fitness.js is AT the 1,500 ratchet and a new check cannot afford a line until task 1003825 buys it back.
|
|
1209
1227
|
];
|
|
1210
1228
|
|
|
@@ -122,9 +122,15 @@ function buildShippedEnvelope(crumb, task) {
|
|
|
122
122
|
title: inputs.title || (task && task.title) || `Task #${crumb.taskId}`,
|
|
123
123
|
valueSummary: inputs.valueSummary || (task && task.value_summary) || '',
|
|
124
124
|
});
|
|
125
|
+
// task 1002518: prefer the rung the shipping session actually resolved, so the
|
|
126
|
+
// later-rendered shipped card matches the one it printed. A breadcrumb written
|
|
127
|
+
// by an older CLI carries only `widgetsEnabled`, which still reads correctly.
|
|
128
|
+
const flags = (crumb.cardFlags && typeof crumb.cardFlags === 'object')
|
|
129
|
+
? crumb.cardFlags
|
|
130
|
+
: { showWidget: crumb.widgetsEnabled !== false, expanded: false };
|
|
125
131
|
return {
|
|
126
|
-
widgets_enabled:
|
|
127
|
-
html: shipCard.renderCardHtml(model),
|
|
132
|
+
widgets_enabled: flags.showWidget === true,
|
|
133
|
+
html: shipCard.renderCardHtml(model, { expanded: flags.expanded === true }),
|
|
128
134
|
markdown: shipCard.renderMonospaceCard(model),
|
|
129
135
|
warnings: [],
|
|
130
136
|
title: `task_${crumb.taskId}_card`,
|