@bongos/core 1.19.509 → 1.19.511
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 +48 -33
- 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 +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +16 -2
- package/docs/adr/0237-a-starter-bundle-is-a-preset-over-the-always-on-core-keyed-by-declared-type.md +137 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +42 -3
- package/docs/api-reference.md +3 -2
- package/docs/module-api-changelog.md +4 -0
- package/modules/copy-desk/proposals.js +13 -0
- package/modules/copy-desk/routes/copy-desk.js +9 -2
- package/modules/copy-desk/tests/copy_proposals.mjs +41 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/lifecycle/conflict-resolve.js +59 -3
- package/modules/provisioning/routes/provisioning.js +19 -0
- package/modules/provisioning/starter-bundles.js +159 -0
- package/modules/public-landing/public/assets/cosmos.css +3 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/copy-apply.js +54 -4
- package/scripts/gds/git-merge-regen.js +18 -0
- package/src/module-api.js +1 -1
- package/tests/conflict_resolve.mjs +118 -4
- package/tests/git_merge_regen.mjs +128 -2
- package/tests/projects_hub.mjs +13 -0
- package/tests/provisioning_starter_bundles.mjs +268 -0
|
@@ -260,8 +260,21 @@ function proposalSourceRef(patch) {
|
|
|
260
260
|
return `copy-proposal-${patch.string_id}-${hash.toString(36)}`;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
// Which version a proposal's task lands in — the version currently BEING BUILT,
|
|
264
|
+
// and nothing else (task 1003493). There is deliberately no fallback: the route
|
|
265
|
+
// used to take `versions[0]` when nothing was building, which is whatever row
|
|
266
|
+
// the query ordered first and may be a shipped version — sealed history that
|
|
267
|
+
// cannot take new tasks. A proposal that lands on a closed version is worse than
|
|
268
|
+
// one that was not accepted, so the caller's answer here is a refusal, not a
|
|
269
|
+
// guess. Pure, so the rule is pinned in copy_proposals.mjs rather than only
|
|
270
|
+
// reachable through a live route.
|
|
271
|
+
function resolveTargetVersion(versions) {
|
|
272
|
+
return (versions || []).find((v) => v && v.status === 'building') || null;
|
|
273
|
+
}
|
|
274
|
+
|
|
263
275
|
module.exports = {
|
|
264
276
|
PROPOSAL_SCHEMA,
|
|
277
|
+
resolveTargetVersion,
|
|
265
278
|
FENCE,
|
|
266
279
|
PLACEHOLDER,
|
|
267
280
|
MIN_WHY_CHARS,
|
|
@@ -391,9 +391,16 @@ module.exports = function buildCopyDeskRouter() {
|
|
|
391
391
|
// how every other unattributed create-vector picks. `createTask` then
|
|
392
392
|
// defaults the goal to that version's catch-all, so a copy change is
|
|
393
393
|
// ordinary work on the board rather than a special case.
|
|
394
|
+
// No `|| versions[0]` fallback (task 1003493) — see resolveTargetVersion:
|
|
395
|
+
// with nothing building, the first row back is whatever the query ordered
|
|
396
|
+
// first, possibly a shipped version, which is sealed history.
|
|
394
397
|
const versions = await lifecycle.listVersions();
|
|
395
|
-
const building =
|
|
396
|
-
if (!building)
|
|
398
|
+
const building = proposals.resolveTargetVersion(versions);
|
|
399
|
+
if (!building) {
|
|
400
|
+
return res.fail('no_open_version', 503, {
|
|
401
|
+
what_this_means: 'No version is currently being built, so there is nowhere for a copy change to land. Nothing was recorded.',
|
|
402
|
+
});
|
|
403
|
+
}
|
|
397
404
|
|
|
398
405
|
const sourceRef = proposals.proposalSourceRef(patch);
|
|
399
406
|
const existing = lifecycle.findTaskBySourceRef
|
|
@@ -275,5 +275,46 @@ await test('the applier re-checks the placeholder arithmetic against the real so
|
|
|
275
275
|
assert.equal(r.code, 'placeholder_mismatch');
|
|
276
276
|
});
|
|
277
277
|
|
|
278
|
+
// --- task 1003493: the two rules the grader found stated but not enforced ----
|
|
279
|
+
|
|
280
|
+
await test('--task proves the claim it names instead of standing in for one', () => {
|
|
281
|
+
const held = [{ task_id: '1003114' }, { task_id: 1003493 }];
|
|
282
|
+
|
|
283
|
+
const wrong = applier.claimGateFor(1003112, held);
|
|
284
|
+
assert.equal(wrong.ok, false);
|
|
285
|
+
assert.equal(wrong.code, 'claim_not_held');
|
|
286
|
+
assert.deepEqual(wrong.detail.held, ['1003114', '1003493'], 'the refusal names the claims you DO hold');
|
|
287
|
+
assert.ok(/claim.js 1003112/.test(wrong.detail.what_this_means), 'and the line that fixes it');
|
|
288
|
+
|
|
289
|
+
// A held claim passes, and an id given as a number matches a claim stored as
|
|
290
|
+
// a string — the ids arrive as both, and a type mismatch here would refuse
|
|
291
|
+
// the legitimate call, which is the failure mode that gets a check removed.
|
|
292
|
+
assert.equal(applier.claimGateFor(1003493, held).ok, true);
|
|
293
|
+
assert.equal(applier.claimGateFor('1003114', held).ok, true);
|
|
294
|
+
|
|
295
|
+
// Holding nothing at all cannot pass, whatever id is named.
|
|
296
|
+
assert.equal(applier.claimGateFor(1003114, []).ok, false);
|
|
297
|
+
assert.equal(applier.claimGateFor(1003114, undefined).ok, false);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
await test('a proposal refuses rather than landing on an arbitrary version', () => {
|
|
301
|
+
const shipped = { id: 'BONGOS-V0', status: 'shipped' };
|
|
302
|
+
const building = { id: 'BONGOS-V1', status: 'building' };
|
|
303
|
+
const planning = { id: 'BONGOS-V2', status: 'planning' };
|
|
304
|
+
|
|
305
|
+
// The version being built wins wherever it sits in the list.
|
|
306
|
+
assert.equal(proposals.resolveTargetVersion([shipped, building, planning]).id, 'BONGOS-V1');
|
|
307
|
+
assert.equal(proposals.resolveTargetVersion([building]).id, 'BONGOS-V1');
|
|
308
|
+
|
|
309
|
+
// With nothing building there is NO fallback — not the first row, not a
|
|
310
|
+
// shipped version, not a planning one. null is what makes the route's
|
|
311
|
+
// no_open_version refusal reachable.
|
|
312
|
+
assert.equal(proposals.resolveTargetVersion([shipped, planning]), null);
|
|
313
|
+
assert.equal(proposals.resolveTargetVersion([shipped]), null);
|
|
314
|
+
assert.equal(proposals.resolveTargetVersion([]), null);
|
|
315
|
+
assert.equal(proposals.resolveTargetVersion(null), null);
|
|
316
|
+
assert.equal(proposals.resolveTargetVersion([null, undefined]), null);
|
|
317
|
+
});
|
|
318
|
+
|
|
278
319
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
279
320
|
process.exit(failed ? 1 : 0);
|
|
@@ -670,6 +670,8 @@ function createClient(opts = {}) {
|
|
|
670
670
|
getProvisioningOnboardPlan: (args) => request("GET", "/provisioning/onboard-plan", { hasBody: false }, args),
|
|
671
671
|
// GET /provisioning/slug-available — rank: any-builder — GET /provisioning/slug-available
|
|
672
672
|
getProvisioningSlugAvailable: (args) => request("GET", "/provisioning/slug-available", { hasBody: false }, args),
|
|
673
|
+
// GET /provisioning/starter-bundles — rank: public — GET /provisioning/starter-bundles
|
|
674
|
+
getProvisioningStarterBundles: (args) => request("GET", "/provisioning/starter-bundles", { hasBody: false }, args),
|
|
673
675
|
// GET /provisioning/tls-check — rank: public — GET /provisioning/tls-check
|
|
674
676
|
getProvisioningTlsCheck: (args) => request("GET", "/provisioning/tls-check", { hasBody: false }, args),
|
|
675
677
|
},
|
|
@@ -138,11 +138,13 @@ async function hasConflictMarkers(gitIn) {
|
|
|
138
138
|
// resolveBranchConflicts — attempt to auto-resolve a generated-file / append-index
|
|
139
139
|
// merge conflict on `branch` against origin/main, server-side. Returns a structured
|
|
140
140
|
// result (never throws for the expected outcomes; throws only on misconfiguration):
|
|
141
|
-
// { resolved: true, head_sha, branch }
|
|
141
|
+
// { resolved: true, head_sha, branch, regenerated } — merged + pushed
|
|
142
142
|
// { resolved: false, skipped: '<why>' [, conflicted: [...] ] } — left as-is
|
|
143
143
|
// skipped reasons: disabled | unconfigured | already_up_to_date | clone_failed |
|
|
144
144
|
// fetch_failed | checkout_failed | markers_remain | push_failed |
|
|
145
|
-
// unresolvable_conflict (with conflicted[]).
|
|
145
|
+
// unresolvable_conflict (with conflicted[]) | generator_failed | commit_failed.
|
|
146
|
+
// `regenerated` says whether step 7b had to commit post-merge generator drift on top
|
|
147
|
+
// of the merge (task 1003546) — head_sha is that commit when it did.
|
|
146
148
|
async function resolveBranchConflicts({ branch } = {}, deps = {}) {
|
|
147
149
|
// Kill-switch: deps.disabled wins for tests; otherwise the env flag (matches the
|
|
148
150
|
// publish-reconciler's PUBLISH_RECONCILER_DISABLED idiom).
|
|
@@ -163,6 +165,7 @@ async function resolveBranchConflicts({ branch } = {}, deps = {}) {
|
|
|
163
165
|
const exec = deps.exec || defaultExec;
|
|
164
166
|
const repoRoot = deps.repoRoot || REPO_ROOT;
|
|
165
167
|
const register = deps.registerMergeDriver || registerMergeDriver;
|
|
168
|
+
const runGenerators = deps.runGenerators || defaultRunGenerators;
|
|
166
169
|
const tmp = (deps.mkdtemp || (() => fs.mkdtempSync(path.join(os.tmpdir(), 'otb-resolve-'))))();
|
|
167
170
|
const work = path.join(tmp, 'work');
|
|
168
171
|
const git = (args, opts) => exec(['-C', work, ...args], opts);
|
|
@@ -225,6 +228,49 @@ async function resolveBranchConflicts({ branch } = {}, deps = {}) {
|
|
|
225
228
|
// not our case (mergeIfGreen reported a conflict), so report a no-op.
|
|
226
229
|
const after = (await git(['rev-parse', 'HEAD'])).stdout.trim();
|
|
227
230
|
if (after === before) return { resolved: false, skipped: 'already_up_to_date' };
|
|
231
|
+
let head = after;
|
|
232
|
+
|
|
233
|
+
// 7b. REGENERATE ON THE MERGED TREE — the step that makes step 6 trustworthy
|
|
234
|
+
// (task 1003546). The `otb-regen` driver is invoked BY git, DURING the merge,
|
|
235
|
+
// and every artifact it rebuilds is produced by SCANNING THE WORKING TREE
|
|
236
|
+
// (copy-inventory.js, gen-repo-map.js, gen-file-map.js). At driver time that
|
|
237
|
+
// tree is still the PRE-merge (ours) tree, so the driver writes an artifact
|
|
238
|
+
// describing the branch, git then completes the merge underneath it, and the
|
|
239
|
+
// result is a clean merge — exit 0, no unmerged paths, no markers — carrying a
|
|
240
|
+
// generated file that matches NEITHER side nor the merged tree. Nothing
|
|
241
|
+
// downstream can catch it: it is only detectable by regenerating again.
|
|
242
|
+
//
|
|
243
|
+
// Measured on the real repo (task 1003546): two branches each shifting lines
|
|
244
|
+
// in modules/hall-ui/public/task-detail.js merge clean, and the merge commit's
|
|
245
|
+
// docs/copy-registry.json is byte-identical to ours — the other side's line
|
|
246
|
+
// numbers are silently DISCARDED (`"line": 464` where the merged tree says
|
|
247
|
+
// 466). That is the PR #536 / task 1003544 strand exactly. Task 1003543 had
|
|
248
|
+
// given these files the driver, which converted the loud CONFLICT they used to
|
|
249
|
+
// throw into this silent staleness — strictly harder to see.
|
|
250
|
+
//
|
|
251
|
+
// A line-numbered or aggregate-carrying artifact cannot be resolved from the
|
|
252
|
+
// three merge sides by construction; the only correct value is a fresh scan of
|
|
253
|
+
// the FINAL tree. So we rebuild here, after the merge commit exists, and
|
|
254
|
+
// commit any drift on top. Ordinary case is a no-op (the driver's guess was
|
|
255
|
+
// right, or nothing generated moved) and we push the merge alone.
|
|
256
|
+
const gen = await runGenerators({ work });
|
|
257
|
+
if (gen && gen.code !== 0) {
|
|
258
|
+
// A generator hard-error / a missing hand-written note (gen-file-map exit 2).
|
|
259
|
+
// We cannot certify the merged tree, and pushing an uncertifiable one is what
|
|
260
|
+
// stranded #536 — leave it, exactly the module's "any failure is a no-op for
|
|
261
|
+
// the strand" posture. The reconciler's attempt cap still escalates to a human.
|
|
262
|
+
return { resolved: false, skipped: 'generator_failed' };
|
|
263
|
+
}
|
|
264
|
+
const drift = (await git(['status', '--porcelain'])).stdout.trim();
|
|
265
|
+
if (drift) {
|
|
266
|
+
await git(['add', '-A']);
|
|
267
|
+
const c = await git([
|
|
268
|
+
'commit', '-m',
|
|
269
|
+
'chore(gds): regenerate generated artifacts from the merged tree (conflict-resolve self-heal, task 1003546)',
|
|
270
|
+
]);
|
|
271
|
+
if (c.code !== 0) return { resolved: false, skipped: 'commit_failed' };
|
|
272
|
+
head = (await git(['rev-parse', 'HEAD'])).stdout.trim();
|
|
273
|
+
}
|
|
228
274
|
|
|
229
275
|
// 8. Belt-and-braces: a clean `git merge` already implies no unmerged paths and
|
|
230
276
|
// no markers, but assert both before we push anything.
|
|
@@ -238,7 +284,7 @@ async function resolveBranchConflicts({ branch } = {}, deps = {}) {
|
|
|
238
284
|
const p = await git(['push', authUrl, `HEAD:refs/heads/${branch}`]);
|
|
239
285
|
if (p.code !== 0) return { resolved: false, skipped: 'push_failed' };
|
|
240
286
|
|
|
241
|
-
return { resolved: true, head_sha:
|
|
287
|
+
return { resolved: true, head_sha: head, branch, regenerated: Boolean(drift) };
|
|
242
288
|
} finally {
|
|
243
289
|
try {
|
|
244
290
|
(deps.rmtmp || ((d) => fs.rmSync(d, { recursive: true, force: true })))(tmp);
|
|
@@ -255,6 +301,16 @@ const GENERATOR_SCRIPTS = [
|
|
|
255
301
|
'scripts/gds/gen-repo-map.js',
|
|
256
302
|
'scripts/gds/gen-file-map.js',
|
|
257
303
|
'scripts/gds/gen-session-index.js',
|
|
304
|
+
// task 1003546: copy-inventory.js writes docs/copy-registry.json +
|
|
305
|
+
// docs/copy-inventory.md, and its rows carry a `line` per copy string — so ANY
|
|
306
|
+
// merge that shifts lines in a scanned UI file re-stales it and `unit`
|
|
307
|
+
// (tests/copy_inventory.mjs) goes red on the merge ref. Its absence here did not
|
|
308
|
+
// merely fail to heal that: with the other three producing no diff, the caller
|
|
309
|
+
// returns `no_regen_diff`, whose documented meaning is "a REAL check failure, not
|
|
310
|
+
// staleness" — so copy-registry staleness was MISDIAGNOSED as a genuine failure,
|
|
311
|
+
// burned the attempt cap, and announceStrand flagged the task needs_rebase
|
|
312
|
+
// (PR #536 / task 1003544).
|
|
313
|
+
'scripts/gds/copy-inventory.js',
|
|
258
314
|
];
|
|
259
315
|
|
|
260
316
|
// Run the CLONE's generator scripts in WRITE mode against the merged working tree.
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
// POST /provisioning/instances/:id/force-teardown — tear down ANY instance
|
|
22
22
|
// public:
|
|
23
23
|
// GET /provisioning/health — liveness + core version
|
|
24
|
+
// GET /provisioning/onboard-plan — the canonical onboarding step sequence
|
|
25
|
+
// GET /provisioning/starter-bundles — the curated module preset per project type
|
|
24
26
|
//
|
|
25
27
|
// Reaches core ONLY through the doorway src/module-api.js (the one-way rule, ADR
|
|
26
28
|
// 0083); it requires this module's own provisioning.js (same-dir), never a sibling.
|
|
@@ -33,6 +35,7 @@ const auth = api;
|
|
|
33
35
|
const { pool, validateOrRespond, parseId } = api;
|
|
34
36
|
const provisioning = require('../provisioning');
|
|
35
37
|
const { onboardPlan, stepsForShape } = require('../onboard-plan');
|
|
38
|
+
const { ALWAYS_ON_CORE, allStarterBundles } = require('../starter-bundles');
|
|
36
39
|
const { makeAskRateLimiter, clientIp } = require('../rate-limit');
|
|
37
40
|
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
38
41
|
const log = api.logger('provisioning');
|
|
@@ -1263,6 +1266,22 @@ module.exports = function provisioningRoutes() {
|
|
|
1263
1266
|
res.json({ mode, steps: stepsForShape(onboardPlan(mode, { domain }), shape) });
|
|
1264
1267
|
});
|
|
1265
1268
|
|
|
1269
|
+
// GET /provisioning/starter-bundles — the curated module preset for each declared
|
|
1270
|
+
// project type (BV1.R19, task 1002338; goal 1000046 criterion `proj-module-select`).
|
|
1271
|
+
// Returns the always-on core once, then one resolved bundle per type:
|
|
1272
|
+
// `{ type, label, summary, core, bundle, modules }`. The whole table in one read,
|
|
1273
|
+
// because the creation picker (R20, task 1002339) re-preselects as the owner flips
|
|
1274
|
+
// the type question and must not refetch to do it; R17's rule engine reads the same
|
|
1275
|
+
// shape so a recommendation and a preselect can never name different modules.
|
|
1276
|
+
// A DECLARATION, not a query — modules/provisioning/starter-bundles.js is pure, so
|
|
1277
|
+
// this route touches no database and no per-request state.
|
|
1278
|
+
// rank: public — the signed-out hub wizard (/projects?view=new) reads it while
|
|
1279
|
+
// choosing a type, exactly like onboard-plan above; no write, no secret, and the
|
|
1280
|
+
// module catalog it names is already public at GET /public/modules.
|
|
1281
|
+
router.get('/provisioning/starter-bundles', (req, res) => {
|
|
1282
|
+
res.json({ core: [...ALWAYS_ON_CORE], bundles: allStarterBundles() });
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1266
1285
|
// GET /provisioning/tls-check?domain=<host> — the Caddy on-demand-TLS ALLOW-LIST
|
|
1267
1286
|
// (ADR 0111 §4, task P4 #1945). Caddy's global `on_demand_tls { ask … }` calls
|
|
1268
1287
|
// this before fetching a Let's Encrypt cert for an SNI it doesn't already have:
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// modules/provisioning/starter-bundles.js — the curated STARTER BUNDLE for each
|
|
2
|
+
// declared project type (BV1.R19, task 1002338; goal 1000046 criterion
|
|
3
|
+
// `proj-module-select`).
|
|
4
|
+
//
|
|
5
|
+
// WHY. Every Cloud Bongos instance boots with the same always-on core — the
|
|
6
|
+
// methodology modules a project cannot sensibly run without. What differs between
|
|
7
|
+
// a game, a research project and a business is the handful of OPT-IN modules on
|
|
8
|
+
// top, and an owner creating their first project has no way to know which of
|
|
9
|
+
// those they want before they have used any of them. A bundle is that opinion,
|
|
10
|
+
// written down once: a project type maps to a short list of opt-in module keys,
|
|
11
|
+
// PRESELECTED for the owner and freely toggled off in the creation picker (R20,
|
|
12
|
+
// task 1002339). It is the answer the flow STARTS from, never a decision it makes
|
|
13
|
+
// on the owner's behalf, and never a thing an owner is stuck with — modules are
|
|
14
|
+
// added and removed after creation too (R23, task 1002342).
|
|
15
|
+
//
|
|
16
|
+
// THE ARROW RUNS TYPE -> MODULES, NEVER BACK. Task 1002772 rejected inferring a
|
|
17
|
+
// project's type from its enabled module set: flags flip all through setup, so a
|
|
18
|
+
// half-configured project would show a confidently wrong type. The type is
|
|
19
|
+
// DECLARED at creation and everything downstream reads off that declaration. R15
|
|
20
|
+
// wrote the same rule into the interview's vocabulary — "the module bundle R19
|
|
21
|
+
// recommends comes off the separately-declared `type`, so the interview
|
|
22
|
+
// deliberately does not ask it a second time" (PROJECT_DETAIL_VOCAB in
|
|
23
|
+
// provisioning.js beside this file).
|
|
24
|
+
//
|
|
25
|
+
// A BUNDLE IS NOT A PUBLISH PRECONDITION. ADR 0182 D4 fixes the publish bar at
|
|
26
|
+
// identity alone — a name and a description, "not tagline, art, modules,
|
|
27
|
+
// governance, or a completed interview" — so taking or declining a bundle must
|
|
28
|
+
// never change whether a project is on the map.
|
|
29
|
+
//
|
|
30
|
+
// PURE — no I/O, no core imports, no sibling-module imports; safe to require from
|
|
31
|
+
// a route, a script or a test, exactly like onboard-plan.js beside it. That purity
|
|
32
|
+
// is why the module keys below are DECLARED here rather than read off the live
|
|
33
|
+
// catalog (src/module-loader/catalog.js, which a module may not import — the
|
|
34
|
+
// one-way rule, ADR 0083). tests/provisioning_starter_bundles.mjs is the pin that
|
|
35
|
+
// keeps the declaration true: it reads every modules/*/module.json off disk and
|
|
36
|
+
// fails the build if a bundle names a module that does not exist, if a bundle
|
|
37
|
+
// names an always-on module (a preselect that toggles nothing), or if
|
|
38
|
+
// ALWAYS_ON_CORE drifts from the manifests' `default: true` set.
|
|
39
|
+
|
|
40
|
+
'use strict';
|
|
41
|
+
|
|
42
|
+
// The modules every instance gets whichever type it declares — the manifests'
|
|
43
|
+
// `default: true` set (docs/modules-contract.md: "core-domain modules that are
|
|
44
|
+
// `default: true` (always on unless disabled)"). They are the FLOOR a bundle
|
|
45
|
+
// layers on, not part of any bundle, and they are listed here so a creation form
|
|
46
|
+
// can show an owner what they already have without a second round trip.
|
|
47
|
+
const ALWAYS_ON_CORE = Object.freeze([
|
|
48
|
+
'autonomy',
|
|
49
|
+
'builder-settings',
|
|
50
|
+
'copy-desk',
|
|
51
|
+
'economy',
|
|
52
|
+
'government',
|
|
53
|
+
'grading',
|
|
54
|
+
'hall-ui',
|
|
55
|
+
'ideas',
|
|
56
|
+
'lifecycle',
|
|
57
|
+
'memory',
|
|
58
|
+
'onboarding',
|
|
59
|
+
'public-landing',
|
|
60
|
+
'security',
|
|
61
|
+
'sessions',
|
|
62
|
+
'status-ui',
|
|
63
|
+
'ui-design',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
// The control plane's own modules. `platform-identity` is the hub identity
|
|
67
|
+
// provider ("enabled ONLY on the hub instance") and `provisioning` is the surface
|
|
68
|
+
// that stands other instances up — a project created THROUGH the hub is a
|
|
69
|
+
// consumer of both, never a second copy of them. Named so the integrity test can
|
|
70
|
+
// refuse them by construction rather than by anyone remembering.
|
|
71
|
+
const HUB_ONLY_MODULES = Object.freeze(['platform-identity', 'provisioning']);
|
|
72
|
+
|
|
73
|
+
// The curated presets, keyed by the declared project type (the closed vocabulary
|
|
74
|
+
// owner-decided 2026-08-13 and mirrored in provisioning.PROJECT_TYPES + the
|
|
75
|
+
// provisioning_007 CHECK). `label` and `summary` are the owner-facing copy the
|
|
76
|
+
// picker renders — plain language, no module jargon, because the owner choosing
|
|
77
|
+
// here has not met any of these modules yet.
|
|
78
|
+
//
|
|
79
|
+
// The lists are SHORT on purpose. A preselect an owner has to audit is worse than
|
|
80
|
+
// none, and today's opt-in catalog is genuinely small (`agents`, `dev-box`,
|
|
81
|
+
// `discord`); a bundle grows as the catalog does, which is exactly why this is a
|
|
82
|
+
// declaration and not a rule engine. Enabling `dev-box` opens the door to a cloud
|
|
83
|
+
// dev environment — it provisions nothing and bills nothing until a builder asks
|
|
84
|
+
// for a box — so preselecting it costs the owner nothing but a toggle.
|
|
85
|
+
const STARTER_BUNDLES = Object.freeze({
|
|
86
|
+
game: Object.freeze({
|
|
87
|
+
label: 'Game starter',
|
|
88
|
+
summary: 'Somewhere for players to gather, and a place to walk a change through before anyone plays it.',
|
|
89
|
+
modules: Object.freeze(['dev-box', 'discord']),
|
|
90
|
+
}),
|
|
91
|
+
research: Object.freeze({
|
|
92
|
+
label: 'Research starter',
|
|
93
|
+
summary: 'Extra agents to run the reading and the experiments the question needs.',
|
|
94
|
+
modules: Object.freeze(['agents']),
|
|
95
|
+
}),
|
|
96
|
+
business: Object.freeze({
|
|
97
|
+
label: 'Business starter',
|
|
98
|
+
summary: 'A place to try a change before customers see it, and agents to look it over first.',
|
|
99
|
+
modules: Object.freeze(['agents', 'dev-box']),
|
|
100
|
+
}),
|
|
101
|
+
'non-profit': Object.freeze({
|
|
102
|
+
label: 'Non-profit starter',
|
|
103
|
+
summary: 'Somewhere the community around the cause can gather and be heard.',
|
|
104
|
+
modules: Object.freeze(['discord']),
|
|
105
|
+
}),
|
|
106
|
+
'not-sure': Object.freeze({
|
|
107
|
+
label: 'Core only',
|
|
108
|
+
summary: 'Just the core, and nothing else to decide today. Add modules whenever you know what you need.',
|
|
109
|
+
modules: Object.freeze([]),
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// The copy an undeclared project gets. Deliberately NOT one of the bundles above:
|
|
114
|
+
// `type` is nullable because every row predates the field and must never be
|
|
115
|
+
// back-filled with a guess (migration provisioning_007), so "nobody said" has to
|
|
116
|
+
// read differently from "the owner said not sure".
|
|
117
|
+
const UNDECLARED = Object.freeze({
|
|
118
|
+
label: 'Core only',
|
|
119
|
+
summary: 'No project type yet, so nothing is preselected — every project starts with the core.',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Resolve one project type to what the creation flow should preselect:
|
|
123
|
+
// `{ type, label, summary, core, bundle, modules }`, where `bundle` is the type's
|
|
124
|
+
// opt-in additions, `core` the always-on floor, and `modules` the union in that
|
|
125
|
+
// order — what the owner ends up with if they change nothing. An unknown or
|
|
126
|
+
// absent type resolves to the core with `type: null`; this never throws and never
|
|
127
|
+
// invents a type, because a recommendation built on an answer nobody gave is
|
|
128
|
+
// worse than no recommendation. Arrays are fresh copies, so a caller may sort or
|
|
129
|
+
// splice its result without corrupting the frozen declaration.
|
|
130
|
+
function bundleForProjectType(type) {
|
|
131
|
+
const declared = typeof type === 'string' && Object.prototype.hasOwnProperty.call(STARTER_BUNDLES, type)
|
|
132
|
+
? type
|
|
133
|
+
: null;
|
|
134
|
+
const preset = declared ? STARTER_BUNDLES[declared] : UNDECLARED;
|
|
135
|
+
const bundle = declared ? [...STARTER_BUNDLES[declared].modules] : [];
|
|
136
|
+
return {
|
|
137
|
+
type: declared,
|
|
138
|
+
label: preset.label,
|
|
139
|
+
summary: preset.summary,
|
|
140
|
+
core: [...ALWAYS_ON_CORE],
|
|
141
|
+
bundle,
|
|
142
|
+
modules: [...ALWAYS_ON_CORE, ...bundle],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Every curated bundle, resolved, in declaration order — the whole table in one
|
|
147
|
+
// read. The creation picker wants all of them up front so flipping the type
|
|
148
|
+
// question re-preselects without another round trip.
|
|
149
|
+
function allStarterBundles() {
|
|
150
|
+
return Object.keys(STARTER_BUNDLES).map(bundleForProjectType);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
ALWAYS_ON_CORE,
|
|
155
|
+
HUB_ONLY_MODULES,
|
|
156
|
+
STARTER_BUNDLES,
|
|
157
|
+
bundleForProjectType,
|
|
158
|
+
allStarterBundles,
|
|
159
|
+
};
|
|
@@ -1501,7 +1501,9 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
1501
1501
|
#view-new #wizSignin:hover{background:color-mix(in srgb,var(--ink) 88%,var(--bg));}
|
|
1502
1502
|
/* the error alert: the error ink between two hairlines, no plate */
|
|
1503
1503
|
#view-new .alert{background:none;border:0;border-top:1px solid var(--err);border-bottom:1px solid var(--err);border-radius:0;padding:12px 0;font-weight:400;}
|
|
1504
|
-
|
|
1504
|
+
/* one fold for the whole wizard: 840 is where the page's bands and ledger stack
|
|
1505
|
+
(cosmos's createSplit collapse), so the rail folds with them, not before them */
|
|
1506
|
+
@media (max-width:840px){
|
|
1505
1507
|
#view-new .steps{grid-template-columns:repeat(3,1fr);gap:18px 10px;margin-bottom:30px;}
|
|
1506
1508
|
#view-new .steps .snum{font-size:18px;}
|
|
1507
1509
|
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.511",
|
|
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.511",
|
|
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.511",
|
|
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",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
//
|
|
33
33
|
// Run: node scripts/gds/copy-apply.js (the task you are HOLDING — the normal call)
|
|
34
34
|
// node scripts/gds/copy-apply.js --dry-run (say what would change, write nothing)
|
|
35
|
-
// node scripts/gds/copy-apply.js --task 1234 (a
|
|
35
|
+
// node scripts/gds/copy-apply.js --task 1234 (a claim you hold, when you hold more than one)
|
|
36
36
|
// node scripts/gds/copy-apply.js --patch-file p.json (a local patch — used by the tests)
|
|
37
37
|
//
|
|
38
38
|
// WHY THE BARE FORM DEFAULTS TO YOUR CLAIM rather than taking an id. The task a
|
|
@@ -43,6 +43,13 @@
|
|
|
43
43
|
// task. So the bare call reads the claim, and holding no claim is a refusal that
|
|
44
44
|
// says to go and claim the task — the methodology's own rule, enforced by the
|
|
45
45
|
// command instead of restated in its output.
|
|
46
|
+
//
|
|
47
|
+
// --task DOES NOT OPT OUT OF THAT (task 1003493). It selects which of your
|
|
48
|
+
// claims to act under when you hold several; it is checked against the same
|
|
49
|
+
// /me claim list the bare call reads, and refuses `claim_not_held` for a task
|
|
50
|
+
// you do not hold. The one path that never touches a task is --patch-file,
|
|
51
|
+
// which reads a local JSON patch and is the tests' entry point — it lands
|
|
52
|
+
// nothing a proposal task authorised, so there is no claim for it to prove.
|
|
46
53
|
|
|
47
54
|
const fs = require('node:fs');
|
|
48
55
|
const path = require('node:path');
|
|
@@ -208,13 +215,50 @@ function refuse(result) {
|
|
|
208
215
|
return 2;
|
|
209
216
|
}
|
|
210
217
|
|
|
218
|
+
// Every claim this session holds. One reader, so the bare call and --task are
|
|
219
|
+
// answered from the same source of truth — the shape of bug where a rule is
|
|
220
|
+
// enforced on one path and merely stated on the other (task 1003493).
|
|
221
|
+
async function activeClaims() {
|
|
222
|
+
const me = await apiCall('GET', '/api/gds/me');
|
|
223
|
+
if (!me.ok) return { ok: false, code: 'no_session', detail: { status: me.status, run: 'node scripts/gds/setup.js' } };
|
|
224
|
+
return { ok: true, claims: me.data.active_claims || (me.data.active_claim ? [me.data.active_claim] : []) };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// --task <id> names a task; this proves the caller actually HOLDS it. Without
|
|
228
|
+
// this the header's "may only be run under a claim" was true of the bare call
|
|
229
|
+
// and false of the named one — a rule stated three times in prose (here, ADR
|
|
230
|
+
// 0233, modules/copy-desk/CLAUDE.md) and enforced on one of two paths. The
|
|
231
|
+
// refusal names the claims you do hold, because the usual cause is a typo'd id.
|
|
232
|
+
// Pure so the gate is testable without a session: given the id asked for and
|
|
233
|
+
// the claims held, either accept or produce the named refusal.
|
|
234
|
+
function claimGateFor(taskId, claims) {
|
|
235
|
+
const wanted = String(taskId);
|
|
236
|
+
const held = (claims || []).map((c) => String(c.task_id));
|
|
237
|
+
if (held.includes(wanted)) return { ok: true, taskId: wanted };
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
code: 'claim_not_held',
|
|
241
|
+
detail: {
|
|
242
|
+
task: wanted,
|
|
243
|
+
held,
|
|
244
|
+
what_this_means: `A copy change lands under a claim like every other change, and you do not hold one on #${wanted}. Claim it first (node scripts/gds/claim.js ${wanted}), then run this again.`,
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function claimOnTask(taskId) {
|
|
250
|
+
const held = await activeClaims();
|
|
251
|
+
if (!held.ok) return held;
|
|
252
|
+
return claimGateFor(taskId, held.claims);
|
|
253
|
+
}
|
|
254
|
+
|
|
211
255
|
// The task this session is holding. Returns a named refusal rather than an id
|
|
212
256
|
// when there is nothing to act under — "claim it first" is the correct answer,
|
|
213
257
|
// not an inconvenience to route around.
|
|
214
258
|
async function claimedTaskId() {
|
|
215
|
-
const
|
|
216
|
-
if (!
|
|
217
|
-
const claims =
|
|
259
|
+
const held = await activeClaims();
|
|
260
|
+
if (!held.ok) return held;
|
|
261
|
+
const claims = held.claims;
|
|
218
262
|
if (!claims.length) {
|
|
219
263
|
return {
|
|
220
264
|
ok: false,
|
|
@@ -271,6 +315,11 @@ async function main() {
|
|
|
271
315
|
if (!held.ok) return cliExit(refuse(held));
|
|
272
316
|
taskId = held.taskId;
|
|
273
317
|
console.log(`copy-apply: the task you are holding — #${taskId}`);
|
|
318
|
+
} else if (taskId) {
|
|
319
|
+
// --task picks WHICH claim to act under; it never substitutes for holding one.
|
|
320
|
+
const held = await claimOnTask(taskId);
|
|
321
|
+
if (!held.ok) return cliExit(refuse(held));
|
|
322
|
+
taskId = held.taskId;
|
|
274
323
|
}
|
|
275
324
|
|
|
276
325
|
const loaded = await loadPatch({ taskId, patchFile });
|
|
@@ -335,4 +384,5 @@ module.exports = {
|
|
|
335
384
|
locateOccurrence,
|
|
336
385
|
reinjectInterpolations,
|
|
337
386
|
applyPatchToSource,
|
|
387
|
+
claimGateFor,
|
|
338
388
|
};
|
|
@@ -20,6 +20,24 @@
|
|
|
20
20
|
// parts of nested docs; it succeeds ONLY if no conflict markers remain. A genuine
|
|
21
21
|
// hand-written conflict falls through to a normal git conflict — it never invents a
|
|
22
22
|
// resolution or clobbers a real edit.
|
|
23
|
+
//
|
|
24
|
+
// ⚠ THE DRIVER'S OUTPUT IS PROVISIONAL FOR ANY TREE-SCANNING ARTIFACT (task 1003546).
|
|
25
|
+
// Every rule below that rebuilds by SCANNING THE WORKING TREE — copy-inventory.js's
|
|
26
|
+
// registry pair, gen-repo-map's map + nested blocks, gen-file-map's sections — is
|
|
27
|
+
// invoked BY git, DURING the merge, when that tree is still the PRE-merge (ours)
|
|
28
|
+
// tree. So the driver writes an artifact describing the branch, git completes the
|
|
29
|
+
// merge underneath it, and the merge lands CLEAN (exit 0, no unmerged paths, no
|
|
30
|
+
// markers) carrying content that matches neither side nor the merged tree. Measured:
|
|
31
|
+
// two branches each shifting lines in one scanned UI file merge clean and the merge
|
|
32
|
+
// commit's docs/copy-registry.json is byte-identical to ours, silently dropping the
|
|
33
|
+
// other side's line numbers (PR #536 / task 1003544).
|
|
34
|
+
// It is still worth running — it clears the markers so the merge completes at all —
|
|
35
|
+
// but it is NOT the authority. Whoever OWNS the merge must regenerate afterwards, on
|
|
36
|
+
// the final tree, and commit the drift: the server does it in conflict-resolve.js
|
|
37
|
+
// step 7b, and a post-merge `resolveInWorktree` call (ship-preflight-steps.js) is
|
|
38
|
+
// correct for the same reason — by then git has written every cleanly-merged path.
|
|
39
|
+
// Do NOT add a rule here and assume the merge is thereby correct; a line-numbered or
|
|
40
|
+
// aggregate-carrying artifact cannot be resolved from the three merge sides at all.
|
|
23
41
|
|
|
24
42
|
const fs = require('node:fs');
|
|
25
43
|
const os = require('node:os');
|
package/src/module-api.js
CHANGED
|
@@ -45,7 +45,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
45
45
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
46
46
|
// the entry to that file. Look for a version's history there, not here.
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
|
-
const CORE_VERSION = '1.19.
|
|
48
|
+
const CORE_VERSION = '1.19.511'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
49
49
|
|
|
50
50
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
51
51
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|