@bongos/core 1.19.772 → 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 +204 -79
- 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 +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/db-tasks.js +7 -1
- package/modules/lifecycle/dead-deps.js +10 -2
- package/modules/lifecycle/ship-card.js +49 -13
- package/modules/lifecycle/task-list-limits.js +44 -0
- 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/audit-deps.js +38 -4
- 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 +28 -6
- package/scripts/gds/ship-resume.js +32 -4
- 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/ship_resume.mjs +86 -12
- package/tests/sso_checkin_legacy_field_gone.mjs +246 -0
- package/tests/task_list_paging.mjs +178 -0
package/docs/file-map.md
CHANGED
|
@@ -189,6 +189,7 @@
|
|
|
189
189
|
│ ├── smoke-write.sh ← write-path smoke (create→claim→ship round-trip)
|
|
190
190
|
│ ├── backup-dropin-sweep.sh ← (task 1003930) control-plane sweep: retires the temporary task-1003925 backup drop-in from each project whose pinned core carries the fix (1.19.750+), proving one backup succeeds without it first; survey by default, `--apply` to act; re-run after each pin moves
|
|
191
191
|
│ ├── stray-unit-sweep.sh ← (task 1003939) control-plane sweep: the ledger's torn-down slugs that still have a service/backup unit loaded or on disk (a torn-down project holding memory outside the ADR 0285 count); survey by default, `--apply` disables, removes, reloads, reset-failed's; DB/dumps/checkout/Caddy left alone
|
|
192
|
+
│ ├── address-park-sweep.sh ← (task 1003947) control-plane sweep: the ledger's torn-down rows that still have a `<slug>.caddy` block or a hostname not yet parked (rows torn down before task 1002897 taught teardown to do both); survey by default, `--apply` removes the snippets, reloads Caddy and parks each non-reserved hostname at 192.0.2.1 via the DNS hook; the reserved rule, zone and apex come from the installed core
|
|
192
193
|
│ ├── smoke-gds.js ← public-surface + 401-check smoke test (asserts /api/pms/* now 404s — removed in V3.R01-followup #308)
|
|
193
194
|
│ ├── box.js ← (#598/ADR 0031) per-builder dev box lifecycle CLI: provision·park·wake·deprovision·sweep-idle·sweep-dormant·status (dry-run default, --apply to act; rank-gated)
|
|
194
195
|
│ ├── smoke-box.sh ← (#598) hermetic box-lifecycle smoke (no DB/DO/network): runs tests/boxes.mjs + tests/cf_tunnel.mjs (#771 named tunnel) + route-rank + module-load
|
|
@@ -2018,5 +2018,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2018
2018
|
landed since 1.19.770 with no explicit bump. run 35116254725. (task 1002620)
|
|
2019
2019
|
1.19.772 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2020
2020
|
landed since 1.19.771 with no explicit bump. run 35117652607. (task 1002620)
|
|
2021
|
+
1.19.773 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2022
|
+
landed since 1.19.772 with no explicit bump. run 35118731311. (task 1002620)
|
|
2023
|
+
1.19.774 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2024
|
+
landed since 1.19.773 with no explicit bump. run 35132992149. (task 1002620)
|
|
2021
2025
|
---------------------------------------------------------------------------
|
|
2022
2026
|
```
|
package/docs/modules-contract.md
CHANGED
|
@@ -249,7 +249,7 @@ A module puts its SQL files under `modules/<key>/migrations/NNN_description.sql`
|
|
|
249
249
|
|
|
250
250
|
Rules (enforced by the `BV1.R45` fitness check):
|
|
251
251
|
- **Own your namespace** — every table must be prefixed `<key>_*`.
|
|
252
|
-
- **Additive only** — `DROP TABLE`, `DROP COLUMN`, and destructive `ALTER` are rejected by the fitness check.
|
|
252
|
+
- **Additive only** — `DROP TABLE`, `DROP COLUMN`, and destructive `ALTER` are rejected by the fitness check. **One acknowledged exception** (task 1003092): the *contract phase* of an expand-then-contract rename ([ADR 0177](adr/0177-goal-membership-kind.md)) may carry exactly `ALTER TABLE <own_table> DROP COLUMN <legacy>` when the file bears the same `-- forward-unsafe: <reason>` pragma core migrations use — one statement, one column, your own table; `DROP TABLE`, `TRUNCATE` and a bare `DROP CONSTRAINT` stay rejected even under the pragma (`scripts/gds/fitness-migration-contraction.js`).
|
|
253
253
|
- **Disable preserves data** — there is no down-migration; a disabled module's tables simply are never created (or are left untouched).
|
|
254
254
|
|
|
255
255
|
---
|
|
@@ -1,63 +1,280 @@
|
|
|
1
|
-
//
|
|
1
|
+
// modules/builder-settings/render-prefs.js — the per-builder CARD RICHNESS knob
|
|
2
|
+
// (task 1279 as a boolean; levels + profile derivation by task 1002518).
|
|
2
3
|
//
|
|
3
|
-
// Governs
|
|
4
|
+
// Governs how much the deterministic session cards (start.js's /builder-start
|
|
4
5
|
// card + the ship-card lifecycle card: claimed → review → confirmed → shipped)
|
|
5
|
-
//
|
|
6
|
-
// the plain
|
|
6
|
+
// spend on the reader: a rich in-feed HTML widget (via the app's show_widget
|
|
7
|
+
// tool), the plain monospace fallback, or nothing but the guaranteed one-line
|
|
8
|
+
// confirmation.
|
|
7
9
|
//
|
|
8
10
|
// Why it's a knob: the widget is nicer, but the agent must relay its full markup
|
|
9
11
|
// VERBATIM into show_widget — reading it once and re-emitting it once — so every
|
|
10
12
|
// byte of HTML is shuttled through the model twice, at every session-start,
|
|
11
13
|
// claim, and ship stage. Measured: the /builder-start widget is ~4.7K tokens vs
|
|
12
14
|
// ~0.5K for the markdown form (9×); the lifecycle card ~0.9K vs ~0.12K monospace
|
|
13
|
-
// (7×). This knob lets a builder
|
|
14
|
-
// emit ONLY the text form (no HTML is ever written or relayed) — so it's a real
|
|
15
|
-
// saving, not just a visual change.
|
|
15
|
+
// (7×). This knob lets a builder spend or save that.
|
|
16
16
|
//
|
|
17
17
|
// Unlike the wandering knob ([[wandering-prefs]]) this is pure cost/taste, NOT a
|
|
18
|
-
// trust gate — so there is NO rank cap: any builder may pick
|
|
19
|
-
//
|
|
18
|
+
// trust gate — so there is NO rank cap: any builder may pick any level.
|
|
19
|
+
//
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// WHY LEVELS AND NOT A BOOLEAN (task 1002518, clause 6 of criterion
|
|
22
|
+
// `accessibility-fluency-profile`).
|
|
23
|
+
//
|
|
24
|
+
// The boolean conflated two independent questions — "how much explanation do I
|
|
25
|
+
// want?" and "how many tokens will I spend?" — and answered both with one
|
|
26
|
+
// switch. That left no way to ask for LESS noise without dropping to the plain
|
|
27
|
+
// text card, and no way to ask for MORE explanation than the widget's default
|
|
28
|
+
// (whose summary ships collapsed, so the part that actually explains the work is
|
|
29
|
+
// behind a click the reader has to know to make).
|
|
30
|
+
//
|
|
31
|
+
// The deeper problem is that it was the ONE surface knob that ignored the
|
|
32
|
+
// interaction profile. The builder who most needs an explanatory card — the
|
|
33
|
+
// non-technical one, who by definition has not opened Settings — got exactly the
|
|
34
|
+
// same card as an engineer who wants a one-liner and nothing else. That is the
|
|
35
|
+
// same failure interaction-prefs.js names in its own preamble: a profile that
|
|
36
|
+
// worked for everyone who had already opted out of it. Richness is not cost/taste
|
|
37
|
+
// alone; it is a fluency question, and fluency already has a profile. So the
|
|
38
|
+
// level DERIVES from that profile, and the knob becomes the override rather than
|
|
39
|
+
// the only input.
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
//
|
|
42
|
+
// THE LADDER, least to most. The ordering is the contract: index is richness,
|
|
43
|
+
// and every consumer asks a predicate (showsCard / showsWidget / widgetExpanded)
|
|
44
|
+
// rather than comparing level strings, so inserting a rung later is additive.
|
|
45
|
+
//
|
|
46
|
+
// minimal the guaranteed [otb-confirm] line only — no card at all.
|
|
47
|
+
// text the monospace / markdown card. No HTML is written or relayed.
|
|
48
|
+
// standard the HTML widget, summary collapsed. (the historical `true`)
|
|
49
|
+
// rich the HTML widget, summary open on arrival — the explanation is
|
|
50
|
+
// the point for this reader, so it is not hidden behind a click.
|
|
51
|
+
//
|
|
52
|
+
// `text` is the historical `false` and `standard` the historical `true`, so a
|
|
53
|
+
// builder who already flipped the toggle keeps precisely the card they chose.
|
|
20
54
|
//
|
|
21
55
|
// Storage: one reserved key ('widgets') inside the `behavior_prefs` jsonb column
|
|
22
|
-
// on `builders` (migration 119 — the same blob the wandering
|
|
23
|
-
//
|
|
24
|
-
//
|
|
56
|
+
// on `builders` (migration 119 — the same blob the wandering and interaction
|
|
57
|
+
// knobs use). The value is a level STRING; the legacy booleans are still read
|
|
58
|
+
// and still mean what they meant. A missing key falls back to the DERIVED level,
|
|
59
|
+
// so the column stays sparse and a builder who never touches this gets a card
|
|
60
|
+
// pitched at the profile they did set.
|
|
25
61
|
//
|
|
26
62
|
// This module is the single source of truth: GET/PATCH /api/gds/me/render + the
|
|
27
63
|
// /me payload surface it, the /settings UI reads it, and the start/claim/ship
|
|
28
|
-
// CLIs read the resolved
|
|
64
|
+
// CLIs read the resolved level to decide what to emit.
|
|
65
|
+
|
|
66
|
+
const interactionPrefs = require('./interaction-prefs');
|
|
29
67
|
|
|
30
68
|
const RESERVED_KEY = 'widgets';
|
|
69
|
+
|
|
70
|
+
// Ordered least → most rich. Index IS the richness rank; see the ladder note.
|
|
71
|
+
const LEVELS = ['minimal', 'text', 'standard', 'rich'];
|
|
72
|
+
|
|
73
|
+
// The historical boolean default (ON). Kept exported because the legacy boolean
|
|
74
|
+
// surface is still live: describeForApi still ships `widgets_enabled`, and CLIs
|
|
75
|
+
// pinned to an older core still read only that. It is also the honest answer for
|
|
76
|
+
// the unset default builder, whose derived level is `rich` — i.e. enabled.
|
|
31
77
|
const DEFAULT_ENABLED = true;
|
|
32
78
|
|
|
33
|
-
//
|
|
79
|
+
// The level each legacy boolean means. Chosen so an existing override is a
|
|
80
|
+
// no-op: whoever set the toggle keeps the exact card they picked.
|
|
81
|
+
const LEGACY_LEVEL = { true: 'standard', false: 'text' };
|
|
34
82
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
83
|
+
// ---- the ladder ------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
function isLevel(value) {
|
|
86
|
+
return typeof value === 'string' && LEVELS.includes(value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function levelRank(level) {
|
|
90
|
+
const i = LEVELS.indexOf(level);
|
|
91
|
+
return i === -1 ? LEVELS.indexOf('standard') : i;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function clampRank(rank) {
|
|
95
|
+
return Math.max(0, Math.min(LEVELS.length - 1, rank));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---- predicates: what a level actually means to a card emitter -------------
|
|
99
|
+
// Consumers ask these, never `level === 'rich'`, so the ladder can grow.
|
|
100
|
+
|
|
101
|
+
// Does anything beyond the one-line [otb-confirm] get printed?
|
|
102
|
+
function showsCard(level) {
|
|
103
|
+
return levelRank(level) >= levelRank('text');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Is HTML written and relayed into show_widget at all? This is the predicate the
|
|
107
|
+
// historical boolean was, and widgetsEnabled() is its alias.
|
|
108
|
+
function showsWidget(level) {
|
|
109
|
+
return levelRank(level) >= levelRank('standard');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Does the widget arrive with its summary already open?
|
|
113
|
+
function widgetExpanded(level) {
|
|
114
|
+
return levelRank(level) >= levelRank('rich');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// The three predicates as one object, which is how a card emitter consumes them.
|
|
118
|
+
//
|
|
119
|
+
// WHY THIS SHAPE EXISTS: modules/lifecycle/ship-card.js draws the cards but may
|
|
120
|
+
// not require this file — a module imports only src/module-api.js, never a
|
|
121
|
+
// sibling module (ADR 0083, enforced by fitness.js). So the ladder cannot travel
|
|
122
|
+
// as a level string that lifecycle would have to interpret; it travels as
|
|
123
|
+
// answers. The CLI scripts (outside modules/, free to require both) call this
|
|
124
|
+
// and hand the result to emitCard. One ladder, one owner, no duplicated rungs.
|
|
125
|
+
function cardFlags(level) {
|
|
126
|
+
return {
|
|
127
|
+
level,
|
|
128
|
+
showCard: showsCard(level),
|
|
129
|
+
showWidget: showsWidget(level),
|
|
130
|
+
expanded: widgetExpanded(level),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---- derivation from the interaction profile -------------------------------
|
|
135
|
+
//
|
|
136
|
+
// The interaction profile lives in the SAME behavior_prefs blob (reserved key
|
|
137
|
+
// 'interaction'), so nothing here needs a second argument or a second read.
|
|
138
|
+
//
|
|
139
|
+
// The rule, stated so it can be argued with:
|
|
140
|
+
// base technical_level is the primary axis — it is the field that says how
|
|
141
|
+
// much explanation the reader wants. non_technical → rich, mixed →
|
|
142
|
+
// standard, engineer → text.
|
|
143
|
+
// -1 machinery: quiet. That knob's whole content is "report outcomes, not
|
|
144
|
+
// mechanism", and a lifecycle card IS mechanism — stage ribbons, claim
|
|
145
|
+
// bookkeeping, grade rows. A reader who asked for less of that should
|
|
146
|
+
// not be handed the richest possible rendering of it.
|
|
147
|
+
// +1 verbosity: detailed. Asks for fuller explanation; the card is where
|
|
148
|
+
// this knob can actually deliver some.
|
|
149
|
+
//
|
|
150
|
+
// verbosity: concise is deliberately NOT -1. It is the DEFAULT, so subtracting
|
|
151
|
+
// for it would push the default builder from `rich` down to `standard` — the
|
|
152
|
+
// accessible-by-default reader would get the less explanatory card, which is the
|
|
153
|
+
// exact inversion this task exists to fix. technical_level already sets the base
|
|
154
|
+
// low for readers who want terseness; concise must not tax them twice.
|
|
155
|
+
const BASE_BY_TECHNICAL_LEVEL = {
|
|
156
|
+
non_technical: 'rich',
|
|
157
|
+
mixed: 'standard',
|
|
158
|
+
engineer: 'text',
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// The level a builder gets with no override — derived from their interaction
|
|
162
|
+
// profile. Pure; takes the same behavior_prefs blob as everything else here.
|
|
163
|
+
function derivedLevel(behaviorPrefs) {
|
|
164
|
+
const profile = interactionPrefs.effectiveProfile(behaviorPrefs);
|
|
165
|
+
const base = BASE_BY_TECHNICAL_LEVEL[profile.technical_level] || 'standard';
|
|
166
|
+
let rank = levelRank(base);
|
|
167
|
+
if (profile.machinery === 'quiet') rank -= 1;
|
|
168
|
+
if (profile.verbosity === 'detailed') rank += 1;
|
|
169
|
+
return LEVELS[clampRank(rank)];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// A short, human sentence saying WHY the derived level is what it is — the
|
|
173
|
+
// /settings panel shows it so "default" is never a black box.
|
|
174
|
+
function derivedReason(behaviorPrefs) {
|
|
175
|
+
const profile = interactionPrefs.effectiveProfile(behaviorPrefs);
|
|
176
|
+
const bits = [`technical level: ${profile.technical_level.replace(/_/g, '-')}`];
|
|
177
|
+
if (profile.machinery === 'quiet') bits.push('machinery: quiet');
|
|
178
|
+
if (profile.verbosity === 'detailed') bits.push('reply length: detailed');
|
|
179
|
+
return `From your interaction profile (${bits.join(', ')}).`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---- overrides -------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
// The raw override a builder has stored, as a LEVEL, or null when unset/invalid.
|
|
185
|
+
// Legacy booleans are read through LEGACY_LEVEL so an old stored value keeps its
|
|
186
|
+
// meaning. Kept separate from the effective value so the UI can show
|
|
187
|
+
// "you chose X" vs "the default (Y)".
|
|
188
|
+
function overrideLevel(behaviorPrefs) {
|
|
39
189
|
const raw = behaviorPrefs && behaviorPrefs[RESERVED_KEY];
|
|
40
|
-
|
|
190
|
+
if (isLevel(raw)) return raw;
|
|
191
|
+
if (typeof raw === 'boolean') return LEGACY_LEVEL[String(raw)];
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The resolved level — the value the CLIs act on. Override if set, else derived.
|
|
196
|
+
function resolveLevel(behaviorPrefs) {
|
|
197
|
+
return overrideLevel(behaviorPrefs) || derivedLevel(behaviorPrefs);
|
|
41
198
|
}
|
|
42
199
|
|
|
43
|
-
//
|
|
44
|
-
//
|
|
200
|
+
// ---- the legacy boolean surface --------------------------------------------
|
|
201
|
+
// Unchanged in meaning and still the shape every pinned CLI reads: true exactly
|
|
202
|
+
// when the resolved level writes and relays HTML.
|
|
45
203
|
function widgetsEnabled(behaviorPrefs) {
|
|
46
|
-
|
|
47
|
-
|
|
204
|
+
return showsWidget(resolveLevel(behaviorPrefs));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// The legacy override accessor: the boolean a caller would have seen before
|
|
208
|
+
// levels existed. `minimal`/`text` → false, `standard`/`rich` → true, unset →
|
|
209
|
+
// null. Preserved so nothing reading `override` has to learn the ladder.
|
|
210
|
+
function overrideEnabled(behaviorPrefs) {
|
|
211
|
+
const lvl = overrideLevel(behaviorPrefs);
|
|
212
|
+
return lvl === null ? null : showsWidget(lvl);
|
|
48
213
|
}
|
|
49
214
|
|
|
50
|
-
//
|
|
215
|
+
// ---- the API payload -------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
const LEVEL_META = {
|
|
218
|
+
minimal: {
|
|
219
|
+
label: 'Minimal',
|
|
220
|
+
blurb: 'One confirmation line and nothing else. The cheapest possible surface.',
|
|
221
|
+
},
|
|
222
|
+
text: {
|
|
223
|
+
label: 'Plain text',
|
|
224
|
+
blurb: 'The monospace card. Same information as the widget, no HTML relayed — cheaper.',
|
|
225
|
+
},
|
|
226
|
+
standard: {
|
|
227
|
+
label: 'Rich card',
|
|
228
|
+
blurb: 'The in-feed widget, with the detail folded behind a summary toggle.',
|
|
229
|
+
},
|
|
230
|
+
rich: {
|
|
231
|
+
label: 'Rich card, expanded',
|
|
232
|
+
blurb: 'The in-feed widget with the summary already open — most explanatory, most tokens.',
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
function levelCatalog() {
|
|
237
|
+
return LEVELS.map((value) => ({
|
|
238
|
+
value,
|
|
239
|
+
label: LEVEL_META[value].label,
|
|
240
|
+
blurb: LEVEL_META[value].blurb,
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Everything the /me payload + the /settings UI need in one object. The four
|
|
245
|
+
// legacy keys stay first and stay exactly what they were: a CLI pinned to an
|
|
246
|
+
// older core reads `widgets_enabled` and is correct.
|
|
51
247
|
function describeForApi(behaviorPrefs) {
|
|
248
|
+
// Resolve each fact ONCE. resolveLevel/overrideEnabled/derivedReason each
|
|
249
|
+
// re-walk the same prefs blob, so calling them field-by-field re-derived the
|
|
250
|
+
// override and the interaction profile two or three times per request.
|
|
251
|
+
const override = overrideLevel(behaviorPrefs);
|
|
252
|
+
const derived = derivedLevel(behaviorPrefs);
|
|
253
|
+
const level = override || derived;
|
|
52
254
|
return {
|
|
53
|
-
widgets_enabled:
|
|
255
|
+
widgets_enabled: showsWidget(level),
|
|
54
256
|
default_enabled: DEFAULT_ENABLED,
|
|
55
|
-
override:
|
|
257
|
+
override: override === null ? null : showsWidget(override),
|
|
258
|
+
// --- levels (task 1002518) ---
|
|
259
|
+
level,
|
|
260
|
+
// The three booleans a card emitter needs, RESOLVED SERVER-SIDE. This is not
|
|
261
|
+
// a convenience: scripts/gds/start.js runs on a task-scoped dev box that
|
|
262
|
+
// carries scripts/ and src/bongos/ but not every modules/ directory (task
|
|
263
|
+
// 1003098), so a CLI cannot require this file to learn what a rung draws —
|
|
264
|
+
// tree-preflight fails the entry point that tries. Shipping the answers
|
|
265
|
+
// keeps the ladder in one place AND keeps the CLI dependency-free.
|
|
266
|
+
flags: cardFlags(level),
|
|
267
|
+
levels: levelCatalog(),
|
|
268
|
+
derived_level: derived,
|
|
269
|
+
derived_reason: derivedReason(behaviorPrefs),
|
|
270
|
+
override_level: override,
|
|
56
271
|
};
|
|
57
272
|
}
|
|
58
273
|
|
|
59
|
-
// Validate a PATCH body: { widgets: <bool> | null }. `null` clears the
|
|
60
|
-
// (fall back to the
|
|
274
|
+
// Validate a PATCH body: { widgets: <level> | <bool> | null }. `null` clears the
|
|
275
|
+
// override (fall back to the derived level). Booleans are still accepted and
|
|
276
|
+
// still mean what they meant — an older hall build PATCHing `{widgets:false}`
|
|
277
|
+
// must keep working against a newer core. Unknown keys → 422.
|
|
61
278
|
// Returns { ok, patch } or { ok:false, errors }. An empty body is a valid no-op.
|
|
62
279
|
function validatePatch(body) {
|
|
63
280
|
if (body == null || typeof body !== 'object' || Array.isArray(body)) {
|
|
@@ -70,19 +287,34 @@ function validatePatch(body) {
|
|
|
70
287
|
errors.push({ key, error: 'unknown_key', allowed: [RESERVED_KEY] });
|
|
71
288
|
continue;
|
|
72
289
|
}
|
|
73
|
-
if (value === null
|
|
74
|
-
patch[RESERVED_KEY] =
|
|
290
|
+
if (value === null) {
|
|
291
|
+
patch[RESERVED_KEY] = null; // null = clear → derived
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (isLevel(value)) {
|
|
295
|
+
patch[RESERVED_KEY] = value;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
// Normalise the legacy boolean on the way IN, so storage only ever holds a
|
|
299
|
+
// level and the compatibility shim stays at this one boundary.
|
|
300
|
+
if (typeof value === 'boolean') {
|
|
301
|
+
patch[RESERVED_KEY] = LEGACY_LEVEL[String(value)];
|
|
75
302
|
continue;
|
|
76
303
|
}
|
|
77
|
-
errors.push({
|
|
304
|
+
errors.push({
|
|
305
|
+
key,
|
|
306
|
+
error: 'invalid_value',
|
|
307
|
+
value,
|
|
308
|
+
allowed: `one of ${LEVELS.join(', ')} (or a boolean, or null to clear)`,
|
|
309
|
+
});
|
|
78
310
|
}
|
|
79
311
|
if (errors.length > 0) return { ok: false, errors };
|
|
80
312
|
return { ok: true, patch };
|
|
81
313
|
}
|
|
82
314
|
|
|
83
315
|
// Apply a validated patch to a builder's existing behavior_prefs. Pure. null
|
|
84
|
-
// deletes the key (fall back to
|
|
85
|
-
//
|
|
316
|
+
// deletes the key (fall back to derived); a level overwrites. Other keys in the
|
|
317
|
+
// blob (e.g. the wandering and interaction knobs) are left untouched.
|
|
86
318
|
function applyPatch(existing, patch) {
|
|
87
319
|
const next = { ...(existing || {}) };
|
|
88
320
|
for (const [key, value] of Object.entries(patch)) {
|
|
@@ -95,8 +327,24 @@ function applyPatch(existing, patch) {
|
|
|
95
327
|
module.exports = {
|
|
96
328
|
RESERVED_KEY,
|
|
97
329
|
DEFAULT_ENABLED,
|
|
330
|
+
LEVELS,
|
|
331
|
+
// the ladder
|
|
332
|
+
isLevel,
|
|
333
|
+
levelRank,
|
|
334
|
+
showsCard,
|
|
335
|
+
showsWidget,
|
|
336
|
+
widgetExpanded,
|
|
337
|
+
cardFlags,
|
|
338
|
+
levelCatalog,
|
|
339
|
+
// resolution
|
|
340
|
+
derivedLevel,
|
|
341
|
+
derivedReason,
|
|
342
|
+
overrideLevel,
|
|
343
|
+
resolveLevel,
|
|
344
|
+
// the legacy boolean surface
|
|
98
345
|
overrideEnabled,
|
|
99
346
|
widgetsEnabled,
|
|
347
|
+
// api
|
|
100
348
|
describeForApi,
|
|
101
349
|
validatePatch,
|
|
102
350
|
applyPatch,
|
|
@@ -337,14 +337,19 @@ module.exports = function buildBuilderSettingsRouter() {
|
|
|
337
337
|
}
|
|
338
338
|
});
|
|
339
339
|
|
|
340
|
-
// Per-builder
|
|
341
|
-
// jsonb / migration 119).
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
340
|
+
// Per-builder CARD RICHNESS knob (task 1279, a four-rung ladder since task
|
|
341
|
+
// 1002518; shares the behavior_prefs jsonb / migration 119). How much the
|
|
342
|
+
// deterministic session cards spend on the reader: nothing but the guaranteed
|
|
343
|
+
// confirmation line, the plain text card, the in-feed HTML widget, or that
|
|
344
|
+
// widget with its summary already open. The widget is nicer but the agent must
|
|
345
|
+
// relay its full markup verbatim into show_widget (read + re-emit) every
|
|
346
|
+
// session-start / claim / ship stage — a real token cost.
|
|
347
|
+
//
|
|
348
|
+
// No rank cap (pure cost/taste, not a trust gate). The DEFAULT is derived from
|
|
349
|
+
// the builder's interaction profile rather than being a constant, so a builder
|
|
350
|
+
// who never opens this panel still gets a card pitched at the profile they did
|
|
351
|
+
// set. render-prefs.js is the single source of truth for both the ladder and
|
|
352
|
+
// the derivation.
|
|
348
353
|
//
|
|
349
354
|
// rank: any-builder — own profile config; mirrors the skill/sound/wandering routes.
|
|
350
355
|
router.get('/me/render', api.requireBuilder, async (req, res) => {
|
|
@@ -357,10 +362,13 @@ module.exports = function buildBuilderSettingsRouter() {
|
|
|
357
362
|
}
|
|
358
363
|
});
|
|
359
364
|
|
|
360
|
-
// PATCH the render knob. Body: { widgets: <bool> | null } where null
|
|
361
|
-
// override (fall back to the
|
|
362
|
-
//
|
|
363
|
-
//
|
|
365
|
+
// PATCH the render knob. Body: { widgets: <level> | <bool> | null } where null
|
|
366
|
+
// clears the override (fall back to the DERIVED level). The legacy boolean is
|
|
367
|
+
// still accepted and normalised to the rung it always meant, so an older hall
|
|
368
|
+
// build keeps working against this core. Any other key, or a value that is
|
|
369
|
+
// neither a known level nor a boolean nor null → 422. Empty body is a no-op
|
|
370
|
+
// (returns current). Sibling behavior keys (wandering, interaction) are
|
|
371
|
+
// preserved by applyPatch.
|
|
364
372
|
//
|
|
365
373
|
// rank: any-builder — self-service profile edit; same posture as /me/wandering.
|
|
366
374
|
router.patch('/me/render', api.requireBuilder, async (req, res) => {
|
|
@@ -704,6 +704,8 @@ function createClient(opts = {}) {
|
|
|
704
704
|
getProvisioningInstancesId: (args) => request("GET", "/provisioning/instances/{id}", { hasBody: false }, args),
|
|
705
705
|
// PATCH /provisioning/instances/{id}/detail — rank: any-builder — PATCH /provisioning/instances/:id/detail
|
|
706
706
|
patchProvisioningInstancesIdDetail: (args) => request("PATCH", "/provisioning/instances/{id}/detail", { hasBody: true }, args),
|
|
707
|
+
// POST /provisioning/instances/{id}/disconnect — rank: any-builder — POST /provisioning/instances/:id/disconnect
|
|
708
|
+
postProvisioningInstancesIdDisconnect: (args) => request("POST", "/provisioning/instances/{id}/disconnect", { hasBody: true }, args),
|
|
707
709
|
// POST /provisioning/instances/{id}/domain — rank: any-builder — POST /provisioning/instances/:id/domain
|
|
708
710
|
postProvisioningInstancesIdDomain: (args) => request("POST", "/provisioning/instances/{id}/domain", { hasBody: true }, args),
|
|
709
711
|
// POST /provisioning/instances/{id}/force-teardown — rank: metic+archon — POST /provisioning/instances/:id/force-teardown
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// modules/hall-ui/public/settings-render.js — the "Rendering" panel on
|
|
2
|
+
// /builders/settings: how rich the deterministic session cards should be
|
|
3
|
+
// (task 1279's on/off toggle, a four-rung ladder since task 1002518).
|
|
4
|
+
//
|
|
5
|
+
// WHY ITS OWN FILE. It began as one row with one switch inside settings.js. As a
|
|
6
|
+
// ladder it needs a labelled option per rung plus a line explaining what the
|
|
7
|
+
// default resolved to and why — and settings.js sits against the 1500-line size
|
|
8
|
+
// ratchet, so the panel moved out on the settings-sessions.js / settings-
|
|
9
|
+
// interaction.js precedent. Extracting it took settings.js from 1493 to 1425.
|
|
10
|
+
//
|
|
11
|
+
// THE SHAPE WORTH KNOWING: unlike every other picker on this page, "default"
|
|
12
|
+
// here is not a constant — it is DERIVED from the builder's interaction profile
|
|
13
|
+
// (render-prefs.js maps technical level, machinery and reply length onto a
|
|
14
|
+
// rung). So the default option cannot just say "default"; it has to say which
|
|
15
|
+
// rung it landed on and which profile fields put it there, or the reader sees a
|
|
16
|
+
// choice the system made for reasons it never stated. The server ships both
|
|
17
|
+
// facts (`derived_level`, `derived_reason`) precisely so this panel can.
|
|
18
|
+
//
|
|
19
|
+
// PATCH body is { widgets: <level> | null } — null clears the override and
|
|
20
|
+
// returns the builder to the derived rung. The route still accepts the legacy
|
|
21
|
+
// boolean, which is what lets an older hall build keep working against a newer
|
|
22
|
+
// core; this panel only ever sends levels or null.
|
|
23
|
+
|
|
24
|
+
(() => {
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const API = '/api/bongos';
|
|
28
|
+
const { escapeHtml, $ } = window.OTB;
|
|
29
|
+
const toast = (msg) => window.OTB.toast(msg);
|
|
30
|
+
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
31
|
+
|
|
32
|
+
const toAuth = () => window.location.assign(`${API}/auth/web/start`);
|
|
33
|
+
|
|
34
|
+
// The "let the profile decide" option carries the sentinel value '' rather
|
|
35
|
+
// than a level, so the change handler can tell "clear my override" from
|
|
36
|
+
// "pin me to the rung that happens to be the derived one today". They differ:
|
|
37
|
+
// a pinned rung survives a later change to the interaction profile.
|
|
38
|
+
const USE_DEFAULT = '';
|
|
39
|
+
|
|
40
|
+
function labelForLevel(data, level) {
|
|
41
|
+
const found = (Array.isArray(data && data.levels) ? data.levels : []).find((l) => l.value === level);
|
|
42
|
+
return (found && found.label) || level;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function render(data) {
|
|
46
|
+
const wrap = $('#render-rows');
|
|
47
|
+
if (!wrap) return;
|
|
48
|
+
const levels = Array.isArray(data && data.levels) ? data.levels : [];
|
|
49
|
+
if (levels.length === 0) {
|
|
50
|
+
wrap.innerHTML = window.OTBKit.emptyStateHtml('No rendering options registered.');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const override = data.override_level || null;
|
|
54
|
+
const derived = data.derived_level;
|
|
55
|
+
|
|
56
|
+
// The default row first, so the reader meets "you already have a sensible
|
|
57
|
+
// answer, here is what it is" before the list of ways to overrule it.
|
|
58
|
+
const defaultRow =
|
|
59
|
+
`<label class="prow prow--pick" for="render-default">` +
|
|
60
|
+
`<input class="pradio" type="radio" name="render-level" id="render-default"` +
|
|
61
|
+
` value="${USE_DEFAULT}"${override === null ? ' checked' : ''}>` +
|
|
62
|
+
'<span class="prow__main">' +
|
|
63
|
+
`<span class="prow__label">Match my interaction profile` +
|
|
64
|
+
`<span class="prow__tag">default (${escapeHtml(labelForLevel(data, derived))})</span></span>` +
|
|
65
|
+
`<span class="prow__desc">${escapeHtml(data.derived_reason || '')} Change that profile and this follows it.</span>` +
|
|
66
|
+
'</span></label>';
|
|
67
|
+
|
|
68
|
+
const rows = levels.map((l) => {
|
|
69
|
+
const id = `render-level-${l.value}`;
|
|
70
|
+
return (
|
|
71
|
+
`<label class="prow prow--pick" for="${id}">` +
|
|
72
|
+
`<input class="pradio" type="radio" name="render-level" id="${id}"` +
|
|
73
|
+
` value="${escapeHtml(l.value)}"${override === l.value ? ' checked' : ''}>` +
|
|
74
|
+
'<span class="prow__main">' +
|
|
75
|
+
`<span class="prow__label">${escapeHtml(l.label)}</span>` +
|
|
76
|
+
`<span class="prow__desc">${escapeHtml(l.blurb)}</span>` +
|
|
77
|
+
'</span></label>'
|
|
78
|
+
);
|
|
79
|
+
}).join('');
|
|
80
|
+
|
|
81
|
+
wrap.innerHTML =
|
|
82
|
+
'<fieldset class="pgroup" id="render-level-group">' +
|
|
83
|
+
'<legend class="prow__label">Session card richness</legend>' +
|
|
84
|
+
'<p class="prow__desc">How much the claim and ship cards show. Richer cards explain more and cost more tokens; the confirmation line is always there whichever you pick.</p>' +
|
|
85
|
+
defaultRow + rows +
|
|
86
|
+
'</fieldset>';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function save(value, input) {
|
|
90
|
+
const status = $('#render-status');
|
|
91
|
+
if (status) status.textContent = 'Saving…';
|
|
92
|
+
if (input) input.disabled = true;
|
|
93
|
+
try {
|
|
94
|
+
// The sentinel becomes an explicit null — the route's clear-the-override
|
|
95
|
+
// signal. Anything else is a level and is stored as the override.
|
|
96
|
+
const body = { widgets: value === USE_DEFAULT ? null : value };
|
|
97
|
+
const res = await api.request('PATCH', `${API}/me/render`, { body });
|
|
98
|
+
if (res.status === 401) { toAuth(); return; }
|
|
99
|
+
if (!res.ok) throw new Error(`render PATCH → ${res.status}`);
|
|
100
|
+
render(res.data);
|
|
101
|
+
const msg = value === USE_DEFAULT
|
|
102
|
+
? `Saved — following your interaction profile (${labelForLevel(res.data, res.data.derived_level)}).`
|
|
103
|
+
: `Saved — ${labelForLevel(res.data, value)}.`;
|
|
104
|
+
if (status) status.textContent = msg;
|
|
105
|
+
toast(msg);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error('[settings] save render failed', err);
|
|
108
|
+
if (status) status.textContent = 'Could not save. Try again.';
|
|
109
|
+
toast('Could not save your rendering preference. Try again.');
|
|
110
|
+
if (input) input.disabled = false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function setup() {
|
|
115
|
+
try {
|
|
116
|
+
const res = await api.request('GET', `${API}/me/render`);
|
|
117
|
+
if (res.status === 401) { toAuth(); return; }
|
|
118
|
+
if (!res.ok) throw new Error(`render GET → ${res.status}`);
|
|
119
|
+
render(res.data);
|
|
120
|
+
$('#render-scroll').hidden = false;
|
|
121
|
+
// Delegate the change so post-render radios stay wired.
|
|
122
|
+
$('#render-rows').addEventListener('change', (e) => {
|
|
123
|
+
const r = e.target.closest('.pradio');
|
|
124
|
+
if (!r || r.disabled || !r.checked) return;
|
|
125
|
+
save(r.value, r);
|
|
126
|
+
});
|
|
127
|
+
} catch (err) {
|
|
128
|
+
// Its own try/catch, like every other section: a failing endpoint shows
|
|
129
|
+
// this panel's own error rather than taking the settings page down.
|
|
130
|
+
console.error('[settings] failed to load render', err);
|
|
131
|
+
$('#render-scroll').hidden = false;
|
|
132
|
+
$('#render-rows').innerHTML =
|
|
133
|
+
'<p class="voices-empty">Your rendering preference could not be loaded. Reload the page to try again.</p>';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
setup();
|
|
138
|
+
})();
|