@bongos/core 1.19.1014 → 1.19.1016
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 +145 -140
- package/.claude/skills/planning-session/SKILL.md +1 -1
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md +4 -1
- package/docs/adr/0331-the-sky-is-the-ideators-home.md +18 -1
- package/docs/adr/0335-an-invite-is-a-pre-approved-row-on-the-projects-own-instance.md +113 -0
- package/docs/adr/0336-a-guilds-record-is-the-sum-of-what-its-members-already-show.md +201 -0
- package/docs/adr/README.md +2 -0
- package/docs/api/openapi.json +4 -0
- package/docs/api-reference.md +1 -1
- package/docs/architecture.md +4 -1
- package/docs/copy-inventory.md +139 -110
- package/docs/copy-registry.json +872 -296
- package/docs/file-map.md +0 -1
- package/docs/module-api-changelog.md +4 -0
- package/migrations/core_253_idea_filed_via.sql +59 -0
- package/modules/agents/pollers/event-router.js +2 -0
- package/modules/discord/discord-bugs.js +2 -0
- package/modules/discord/discord-inbound.js +3 -0
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +125 -52
- package/modules/hall-ui/public/goal-map.states.json +29 -5
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/people.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +11 -0
- package/modules/hall-ui/public/sky-data.js +41 -9
- package/modules/hall-ui/public/sky-draw.js +3 -2
- package/modules/hall-ui/public/sky-interaction.js +14 -7
- package/modules/hall-ui/public/sky-layout.js +2 -1
- package/modules/hall-ui/public/sky-panel.js +1 -1
- package/modules/hall-ui/public/sky-tour.js +10 -6
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.css +6 -3
- package/modules/hall-ui/public/thinking.html +16 -10
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/ideas/born-of.js +55 -0
- package/modules/ideas/inbox.js +23 -8
- package/modules/ideas/projection.js +48 -0
- package/modules/ideas/routes/inbox.js +20 -0
- package/modules/lifecycle/sky.js +25 -4
- package/modules/platform-identity/activity-snapshots.js +47 -0
- package/modules/platform-identity/migrations/platform_identity_022_activity_snapshots.sql +46 -0
- package/modules/platform-identity/platform-identity.js +13 -0
- package/modules/platform-identity/tests/activity-snapshots.mjs +68 -0
- package/modules/platform-identity/tests/platform-identity.mjs +21 -0
- package/modules/ui-design/skills/impeccable/SKILL.md +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/capture.js +11 -0
- package/scripts/gds/run-unit-tests.js +4 -0
- package/src/bongos/serve-internal.js +4 -0
- package/src/module-api.js +1 -1
- package/tests/activity_snapshots_db.mjs +97 -0
- package/tests/discord_bugs.mjs +1 -0
- package/tests/discord_inbound.mjs +1 -0
- package/tests/goal_map_page.mjs +360 -211
- package/tests/guild_namespace_reserved.mjs +76 -0
- package/tests/hall_audit.mjs +1 -0
- package/tests/hall_nav.mjs +6 -1
- package/tests/hall_page_gate_map.mjs +3 -2
- package/tests/hall_sky.mjs +33 -16
- package/tests/helpers.mjs +21 -5
- package/tests/idea_filed_via.mjs +185 -0
- package/tests/landing_page.mjs +8 -0
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/profile_rollup_consent.mjs +5 -1
- package/tests/project_admin_console_model.mjs +105 -0
- package/tests/render_check.mjs +6 -4
- package/tests/sky_read.mjs +19 -2
- package/tests/ui_design_skills.mjs +9 -0
- package/modules/hall-ui/public/goal-map-lib.js +0 -349
- package/modules/hall-ui/public/goal-map.css +0 -117
- package/modules/hall-ui/public/goal-map.js +0 -232
- package/modules/public-landing/public/world/assets/clouds/cloud-island-large.png +0 -0
- package/modules/public-landing/public/world/assets/clouds/cloud-puff.png +0 -0
- package/modules/public-landing/public/world/assets/creatures/creature-whale-cargo.png +0 -0
- package/modules/public-landing/public/world/assets/creatures/creature-whale-glide.png +0 -0
- package/modules/public-landing/public/world/assets/plates/plate-clouds-near.png +0 -0
- package/modules/public-landing/public/world/assets/plates/scene-establishing.png +0 -0
- package/tests/goal_map_lib.mjs +0 -214
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
|
|
51
51
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
52
52
|
chrome exists before the page script runs. -->
|
|
53
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
53
|
+
<script src="/builders/shell.js?v=2026-09-25-sky4b"></script>
|
|
54
54
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
55
55
|
<script src="/builders/palette.js" defer></script>
|
|
56
56
|
<!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
|
|
100
100
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
101
101
|
chrome exists before the page script runs. -->
|
|
102
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
102
|
+
<script src="/builders/shell.js?v=2026-09-25-sky4b"></script>
|
|
103
103
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
104
104
|
<script src="/builders/palette.js" defer></script>
|
|
105
105
|
<!-- shared frontend toolkit (window.OTB) — loaded BEFORE sessions.js so it can pull from it. -->
|
|
@@ -416,7 +416,7 @@
|
|
|
416
416
|
|
|
417
417
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
418
418
|
chrome exists before the page scripts run. -->
|
|
419
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
419
|
+
<script src="/builders/shell.js?v=2026-09-25-sky4b"></script>
|
|
420
420
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
421
421
|
<script src="/builders/palette.js" defer></script>
|
|
422
422
|
<!-- shared frontend toolkit (window.OTB) — loaded before consuming IIFEs. -->
|
|
@@ -155,6 +155,17 @@
|
|
|
155
155
|
{ id: 'home', label: 'Home', icon: 'home', href: '/', view: 'home', gate: 'always' },
|
|
156
156
|
{ id: 'roadmap', label: 'Roadmap', icon: 'roadmap', href: '/roadmap', gate: 'member' },
|
|
157
157
|
{ id: 'goals', label: 'Goals', icon: 'goals', href: '/goals', gate: 'member' },
|
|
158
|
+
// task 1004232 (sky part 4 of 5, goal 1000095; ADR 0227 §11, ADR 0331):
|
|
159
|
+
// THE PROJECT SKY — the whole project drawn from the ledger, at the URL
|
|
160
|
+
// the goal map held. The map took no nav slot on purpose (task 1003479:
|
|
161
|
+
// reachable from Goals, never load-bearing); the owner granted the sky
|
|
162
|
+
// that slot on 2026-09-24, directly under Goals because it is the goals
|
|
163
|
+
// seen as one picture — the same audience, the project. gate 'member' as
|
|
164
|
+
// Goals is: cosmetic, like every gate here — the page gate is the wall
|
|
165
|
+
// (requireBuilderPage, the tier the map sat on). The id stays 'goal-map'
|
|
166
|
+
// because the href does: Goals' own "see which goals wait on each other"
|
|
167
|
+
// link and every bookmark land here unchanged.
|
|
168
|
+
{ id: 'goal-map', label: 'Sky', icon: 'goals', href: '/goal-map', gate: 'member' },
|
|
158
169
|
// task 1966: Tasks (the board formerly labelled "Work") is visible to ANY
|
|
159
170
|
// signed-in builder (incl. newcomer Xenos). Claimability is controlled
|
|
160
171
|
// per-task, not by hiding the page — every row carries a rank tag and the
|
|
@@ -21,10 +21,31 @@
|
|
|
21
21
|
// instance's version ids; the goal layout; and the thoughts — their state,
|
|
22
22
|
// their placement, their words.
|
|
23
23
|
//
|
|
24
|
-
// TWO LAYERS
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
24
|
+
// TWO LAYERS BECAME LEDGER FACTS IN PART 5 (task 1004233; the README's "must
|
|
25
|
+
// not ship" rows, shipped): the "claude layer" reads `filed_via` off each row of
|
|
26
|
+
// GET /sky/mine — cli, api and discord wait behind the pill, hall and planning
|
|
27
|
+
// always show, and a row from before the column (NULL) shows and says "filed
|
|
28
|
+
// before the ledger recorded where" — and every born-of mark reads `born` off
|
|
29
|
+
// GET /sky (which thoughts became goals or rings, from the ratify tables).
|
|
30
|
+
// Nothing here guesses either from text.
|
|
31
|
+
//
|
|
32
|
+
// ONE PAGE, TWO DOORS (part 4, task 1004232). /thinking and /goal-map are the
|
|
33
|
+
// same seven files over the same markup; the body's `data-sky-lens` says which
|
|
34
|
+
// lens is HOME — 'mine' on /thinking (the ideator's sky), 'all' on /goal-map
|
|
35
|
+
// (the project sky). `S.home` is read once here; sky-tour.js boots into it and
|
|
36
|
+
// hands back to it after the tour, and sky-interaction.js's switcher leaves
|
|
37
|
+
// for the other page rather than switching when the lens asked for lives
|
|
38
|
+
// there. Every lens still exists inside either page, because the tour walks
|
|
39
|
+
// all three.
|
|
40
|
+
//
|
|
41
|
+
// ONE PAGE, TWO DOORS (part 4, task 1004232). /thinking and /goal-map are the
|
|
42
|
+
// same seven files over the same markup; the body's `data-sky-lens` says which
|
|
43
|
+
// lens is HOME — 'mine' on /thinking (the ideator's sky), 'all' on /goal-map
|
|
44
|
+
// (the project sky). `S.home` is read once here; sky-tour.js boots into it and
|
|
45
|
+
// hands back to it after the tour, and sky-interaction.js's switcher leaves
|
|
46
|
+
// for the other page rather than switching when the lens asked for lives
|
|
47
|
+
// there. Every lens still exists inside either page, because the tour walks
|
|
48
|
+
// all three.
|
|
28
49
|
|
|
29
50
|
(() => {
|
|
30
51
|
'use strict';
|
|
@@ -55,6 +76,15 @@
|
|
|
55
76
|
const STOP = new Set('a an the to of in on for and or is it this that with from by as at be are was not no we you your our into onto when what how why should could would can will every each all any some one two page let me without does did has have had its their them then than but so if'.split(' '));
|
|
56
77
|
function tokens(s) { return String(s || '').toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2 && !STOP.has(w)); }
|
|
57
78
|
const toMs = (iso) => iso ? new Date(iso).getTime() : 0;
|
|
79
|
+
// Where an idea was filed from (task 1004233): the five doors the ledger
|
|
80
|
+
// records, which of them a SESSION files through (the "claude layer" hides
|
|
81
|
+
// these until the pill is pressed), and the words the panel says for each.
|
|
82
|
+
const SESSION_VIAS = Object.freeze(['cli', 'api', 'discord']);
|
|
83
|
+
const VIA_WORDS = { hall: 'from the hall', cli: 'from the command line', discord: 'from Discord', planning: 'in a planning session', api: 'through the API' };
|
|
84
|
+
function viaWord(v) { return v == null ? 'filed before the ledger recorded where' : (VIA_WORDS[v] || 'through ' + v); }
|
|
85
|
+
// bornFromSky — GET /sky's `born` as string-keyed, string-valued maps; absent
|
|
86
|
+
// or half-shaped is empty (no mark is drawn on a guess).
|
|
87
|
+
function bornFromSky(b) { const m = (o) => { const out = {}; if (o && typeof o === 'object') for (const [k, v] of Object.entries(o)) if (v != null) out[String(k)] = String(v); return out; }; return { goals: m(b && b.goals), criteria: m(b && b.criteria) }; }
|
|
58
88
|
// The hall's own doors — the prototype linked to a fixed hall origin; the
|
|
59
89
|
// page links to the hall it is served from (apex /builders or the subdomain).
|
|
60
90
|
const hallBase = () => (location.hostname.startsWith('builders.') ? '' : '/builders');
|
|
@@ -78,7 +108,7 @@
|
|
|
78
108
|
baseIdeas: [], ideas: [], becameGoal: new Map(), becameCrit: new Map(), T0: NOW - 86400000,
|
|
79
109
|
hasFiledVia: false, canNod: false, template: null, buildingVersionIds: [], loaded: false,
|
|
80
110
|
rehearsal: false, claudeLayer: false, influence: false, ideasOnMap: false, recentOnly: false,
|
|
81
|
-
lens: 'mine', picked: null, hover: null, filter: 'all', gfilter: 'all', dirty: true, gravityGoal: null, flying: null, tourSpot: null,
|
|
111
|
+
lens: 'mine', home: document.body.dataset.skyLens === 'all' ? 'all' : 'mine', picked: null, hover: null, filter: 'all', gfilter: 'all', dirty: true, gravityGoal: null, flying: null, tourSpot: null,
|
|
82
112
|
clock: NOW, playing: false, clockHidden: store.get('sky-clock') === 'off',
|
|
83
113
|
charted: new Map(), focusSet: null, explore: { on: store.get('sky-explore') !== 'off', seen: new Set(JSON.parse(store.get('sky-seen') || '[]')) },
|
|
84
114
|
cam: { x: 0, y: 0, k: 0.12 }, flight: null, W: 0, H: 0, DPR: 1, t0: 0, shuttles: [],
|
|
@@ -107,9 +137,10 @@
|
|
|
107
137
|
size: i.size || null,
|
|
108
138
|
state: st ? { ...st, wellGoalId: st.wellGoalId != null ? String(st.wellGoalId) : null } : null,
|
|
109
139
|
mergedInto: i.merged_into_idea_id != null ? String(i.merged_into_idea_id) : null,
|
|
110
|
-
// filed_via
|
|
111
|
-
//
|
|
112
|
-
|
|
140
|
+
// filed_via (part 5): a session's door puts the thought behind the claude
|
|
141
|
+
// layer; a person's door, and a row from before the column (NULL — the
|
|
142
|
+
// owner: historic ideas SHOW by default), do not.
|
|
143
|
+
filedVia: i.filed_via ?? null, origin: SESSION_VIAS.includes(i.filed_via) ? 'claude' : 'human',
|
|
113
144
|
};
|
|
114
145
|
}
|
|
115
146
|
|
|
@@ -166,6 +197,7 @@
|
|
|
166
197
|
// Called once, after the reads; everything it derives lands on S.
|
|
167
198
|
function buildWorld({ sky, mine, versions }) {
|
|
168
199
|
S.ASPECT = clamp(innerWidth / Math.max(1, innerHeight), 1, 1.8);
|
|
200
|
+
S.BORN = bornFromSky(sky && sky.born);
|
|
169
201
|
const BORN = S.BORN;
|
|
170
202
|
const goals = ((sky && sky.goals) || []).map(goalFromSky).map((g) => ({ ...g, bornOf: BORN.goals[g.id] || null, born: toMs(g.createdAt) }));
|
|
171
203
|
const goalById = new Map(goals.map((g) => [g.id, g]));
|
|
@@ -281,7 +313,7 @@
|
|
|
281
313
|
|
|
282
314
|
Object.assign(SKY, {
|
|
283
315
|
S, $, TAU, GOLDEN, reduce, NOW, hash, rng, clamp, lerp, smooth, ease, esc, fmtDate, fmtDay, ageDays, plain, short, store, tokens, toMs, hallBase, API, api,
|
|
284
|
-
OBJ, objects, goalFromSky, ideaFromMine, bandsFor, wellRadius, ringR, taskAt, buildWorld, edgesOf,
|
|
316
|
+
OBJ, objects, SESSION_VIAS, viaWord, bornFromSky, goalFromSky, ideaFromMine, bandsFor, wellRadius, ringR, taskAt, buildWorld, edgesOf,
|
|
285
317
|
applyRehearsal, inLayer, shown, diameter, positionWord, brightnessWord, reach, became, temp, placeIdeas, touched, load,
|
|
286
318
|
});
|
|
287
319
|
})();
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
// thicker with credits), bodies are tasks past the zoom threshold and dust
|
|
8
8
|
// below it, and a thought is one of the five objects — its SIZE the credits
|
|
9
9
|
// its descent was paid (never smaller for being dead), its BRIGHTNESS how
|
|
10
|
-
// much of that work still stands. Born-of marks
|
|
11
|
-
//
|
|
10
|
+
// much of that work still stands. Born-of marks read GET /sky's `born` and the
|
|
11
|
+
// "filed by a session" accent reads `filed_via` — ledger facts since part 5
|
|
12
|
+
// (task 1004233), never a guess from text.
|
|
12
13
|
|
|
13
14
|
(() => {
|
|
14
15
|
'use strict';
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
//
|
|
6
6
|
// The prototype's "hover card + shuttle menu", "interaction" and "lenses and
|
|
7
7
|
// toggles" sections, verbatim but for the shared state on `SKY.S`. The one
|
|
8
|
-
// behaviour this part CHANGES on purpose: the
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// behaviour this part CHANGES on purpose: the lens switcher LEAVES for the
|
|
9
|
+
// other page when the lens asked for is that page's home (part 4, task
|
|
10
|
+
// 1004232) — on /thinking, "the whole project" goes to /goal-map; on
|
|
11
|
+
// /goal-map, "simple" and "your thinking" go to /thinking — instead of
|
|
12
|
+
// switching in place. Every lens survives inside both pages because the tour
|
|
13
|
+
// walks all three. And the "claude layer" pill only appears once part 5's
|
|
12
14
|
// `filed_via` exists on at least one thought; until then it is hidden and
|
|
13
15
|
// every thought shows (the README's "must not ship" row).
|
|
14
16
|
//
|
|
@@ -90,9 +92,14 @@
|
|
|
90
92
|
if (!noFly) { if (l === 'all') SKY.fitAll(); else SKY.fitPresent(); }
|
|
91
93
|
SKY.maybeStart(); SKY.renderRoom(); updateStats(); S.dirty = true;
|
|
92
94
|
}
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
|
|
95
|
+
// The switcher is two doors (part 4, task 1004232): a lens that lives on the
|
|
96
|
+
// OTHER page is reached by going there. /thinking is home to 'simple' and
|
|
97
|
+
// 'mine'; /goal-map is home to 'all'. Inside a page every lens still works
|
|
98
|
+
// (the tour walks all three), so only the button leaves.
|
|
99
|
+
const leave = (p) => location.assign(`${hallBase()}${p}`);
|
|
100
|
+
$('lensSimple').addEventListener('click', () => (S.home === 'all' ? leave('/thinking') : setLens('simple')));
|
|
101
|
+
$('lensMine').addEventListener('click', () => (S.home === 'all' ? leave('/thinking') : setLens('mine')));
|
|
102
|
+
$('lensAll').addEventListener('click', () => (S.home === 'all' ? setLens('all') : leave('/goal-map')));
|
|
96
103
|
$('fitBtn').addEventListener('click', () => (S.lens === 'all' ? SKY.fitAll() : SKY.fitPresent()));
|
|
97
104
|
function togglePill(id, get, set) { $(id).addEventListener('click', () => { set(!get()); $(id).setAttribute('aria-pressed', String(get())); SKY.renderRoom(); updateStats(); S.dirty = true; }); }
|
|
98
105
|
togglePill('recentBtn', () => S.recentOnly, (v) => { S.recentOnly = v; });
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// sky-data.js for the split). The clock draws the sky as it stood on a day —
|
|
7
7
|
// nothing moves position, only what exists changes. Routes are the goal
|
|
8
8
|
// graph's own edges, charted by a shuttle when asked. Explore is the project
|
|
9
|
-
// sky's rule (
|
|
9
|
+
// sky's rule (home at /goal-map since part 4; the tour walks it on /thinking
|
|
10
|
+
// too). The camera is a
|
|
10
11
|
// pan/zoom over world coordinates laid out in sky-data.js.
|
|
11
12
|
|
|
12
13
|
(() => {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
${i.size ? fact('of which', esc(sizeWords(i))) : ''}
|
|
42
42
|
${bright ? fact('brightness — still alive', `<span class="bar" style="width:${Math.max(4, Math.round(i.lit * 70))}px;${i.lit > 0 ? '' : 'background:rgba(255,255,255,.2)'}"></span>${esc(bright)}`) : ''}
|
|
43
43
|
${pos ? fact('position', esc(pos)) : ''}
|
|
44
|
-
${fact('filed', esc(fmtDate(i.capturedAt)) + (i.kind ? ` · ${esc(i.kind)}` : '') + (i.grade === 'full' ? ' · full idea' : '') + (i.origin === 'claude' ? ' · by a session, under your name' : ''))}
|
|
44
|
+
${fact('filed', esc(fmtDate(i.capturedAt)) + (i.kind ? ` · ${esc(i.kind)}` : '') + (i.grade === 'full' ? ' · full idea' : '') + ' · ' + esc(SKY.viaWord(i.filedVia)) + (i.origin === 'claude' ? ' · by a session, under your name' : ''))}
|
|
45
45
|
</div>
|
|
46
46
|
${i.fields && FIELDS.length ? `<div><span class="lbl">the five answers · ${SKY.completeness(i.fields)} of ${FIELDS.length} above the floor</span><ul class="orbit-list">${FIELDS.map((f) => `<li><span class="st">${esc(f.key.replace('_', ' '))}</span><span>${esc(i.fields[f.key] || '— skipped')}</span></li>`).join('')}</ul></div>` : ''}
|
|
47
47
|
${i.lineage.length ? `<div><span class="lbl">the descent — everything that fell out of it</span><ul class="orbit-list">${[...i.lineage, ...i.descentTasks].map((t) => `<li><span class="st${t.status === 'active' ? ' live' : ''}">${esc(t.status)}</span><span>${esc(t.title)}${t.goalId && t.goalId !== i.well && goalById.get(t.goalId) ? ` <span class="lbl">in ${esc(short(goalById.get(t.goalId).title))}</span>` : ''}</span></li>`).join('')}</ul></div>` : ''}
|
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
// prototype's own start-up line. The thirteen steps and their words are the
|
|
8
8
|
// design of record's; the tour opens once per browser (`sky-tour-seen`).
|
|
9
9
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
10
|
+
// The tour walks every lens inside whichever page it runs on — steps 7, 8
|
|
11
|
+
// and 10 the project sky's ('all'), steps 1, 11 and 12 the ideator's ('mine'),
|
|
12
|
+
// step 13 'simple' — which is why all three survive in both pages although the
|
|
13
|
+
// switcher leaves for the other page. When it ends it hands back to the page's
|
|
14
|
+
// HOME lens (`S.home`: 'mine' on /thinking, 'all' on /goal-map — part 4, task
|
|
15
|
+
// 1004232), and the boot opens in that lens too.
|
|
13
16
|
|
|
14
17
|
(() => {
|
|
15
18
|
'use strict';
|
|
@@ -43,7 +46,7 @@
|
|
|
43
46
|
function spot(el, add) { if (!add) { S.spotted.forEach((e) => e.classList.remove('spot')); S.spotted = []; } if (el) { el.classList.add('spot'); S.spotted.push(el); } }
|
|
44
47
|
function showStep() { const s = STEPS[S.tourI]; if (!s) return endTour(); spot(null); S.tourSpot = null; s.go(); $('tourN').textContent = `how to read the sky · ${S.tourI + 1} of ${STEPS.length}`; $('tourDots').innerHTML = STEPS.map((_, n) => `<i class="${n === S.tourI ? 'on' : ''}"></i>`).join(''); $('tourH').textContent = s.h; $('tourP').textContent = s.p; $('tourNext').textContent = S.tourI === STEPS.length - 1 ? 'start reading' : 'next'; $('tourBack').hidden = S.tourI === 0; $('tour').hidden = false; S.dirty = true; }
|
|
45
48
|
function startTour() { SKY.pick(null); SKY.toggleRoom(false); SKY.clearCharts(); $('start').hidden = true; S.exploreWas = S.explore.on; S.tourI = 0; showStep(); }
|
|
46
|
-
function endTour() { $('tour').hidden = true; spot(null); S.tourSpot = null; S.tourI = -1; if (S.exploreWas != null) { S.explore.on = S.exploreWas; $('exploreBtn').setAttribute('aria-pressed', String(S.explore.on)); S.exploreWas = null; } store.set('sky-tour-seen', '4'); SKY.setLens(
|
|
49
|
+
function endTour() { $('tour').hidden = true; spot(null); S.tourSpot = null; S.tourI = -1; if (S.exploreWas != null) { S.explore.on = S.exploreWas; $('exploreBtn').setAttribute('aria-pressed', String(S.explore.on)); S.exploreWas = null; } store.set('sky-tour-seen', '4'); SKY.setLens(S.home, true); SKY.clearCharts(); SKY.maybeStart(); S.dirty = true; }
|
|
47
50
|
$('tourNext').addEventListener('click', () => { S.tourI++; showStep(); }); $('tourBack').addEventListener('click', () => { S.tourI = Math.max(0, S.tourI - 1); showStep(); }); $('tourSkip').addEventListener('click', endTour); $('tourBtn').addEventListener('click', startTour);
|
|
48
51
|
|
|
49
52
|
// ---------------------------------------------------------------- the loop
|
|
@@ -68,8 +71,9 @@
|
|
|
68
71
|
// The Full Idea switch needs the template's key to file; without the read there is nothing to file as.
|
|
69
72
|
$('fullToggle').hidden = !S.template;
|
|
70
73
|
S.t0 = performance.now();
|
|
71
|
-
SKY.resize(); SKY.setClock(NOW); SKY.setLens(
|
|
72
|
-
|
|
74
|
+
SKY.resize(); SKY.setClock(NOW); SKY.setLens(S.home, true); SKY.renderPanel(null); $('time').hidden = S.clockHidden;
|
|
75
|
+
// The first frame is the home lens's own fit: the present for your sky, the whole sky for the project's.
|
|
76
|
+
S.cam.k = S.home === 'all' ? SKY.fitRad(S.BANDS[S.BANDS.length - 1].r1 + 100) : SKY.fitRad(S.BANDS[0].r1 + 120);
|
|
73
77
|
document.body.dataset.skyReady = '1';
|
|
74
78
|
requestAnimationFrame(frame);
|
|
75
79
|
if (store.get('sky-tour-seen') !== '4') setTimeout(startTour, 600);
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
|
|
67
67
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
68
68
|
|
|
69
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
69
|
+
<script src="/builders/shell.js?v=2026-09-25-sky4b"></script>
|
|
70
70
|
<script src="/builders/palette.js" defer></script>
|
|
71
71
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
72
72
|
<!-- The kit, for its ONE renderer: the full-size ship-time visual figure
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
|
|
58
58
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
59
59
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
60
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
60
|
+
<script src="/builders/shell.js?v=2026-09-25-sky4b"></script>
|
|
61
61
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
62
62
|
<script src="/builders/palette.js" defer></script>
|
|
63
63
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
prototype as the design of record and this sheet is its pixel contract. It
|
|
5
5
|
deliberately does NOT load the hall's style.css or its tokens — the sky is
|
|
6
6
|
its own surface (black void, Manrope, JetBrains Mono for figures), and the
|
|
7
|
-
template's values are the contract. Keep it one file (the task says so).
|
|
7
|
+
template's values are the contract. Keep it one file (the task says so).
|
|
8
|
+
ONE SHEET, TWO DOORS (part 4, task 1004232): /thinking and /goal-map are the
|
|
9
|
+
same page opened in different lenses, so both bodies are named below and
|
|
10
|
+
nothing in here is per-page. */
|
|
8
11
|
/* The template declared these on :root. They sit on the page's body here,
|
|
9
12
|
with the template's values untouched, because the server splices the
|
|
10
13
|
branding pack's own :root (--ink, --accent, --rule …) into <head> AFTER this
|
|
@@ -14,7 +17,7 @@
|
|
|
14
17
|
its own — tests/hall_reading_world.mjs; color-scheme inherits from body
|
|
15
18
|
too). Same reason `body` below is qualified: a pack backdrop is written as
|
|
16
19
|
`body{background:…}` and the void must win. */
|
|
17
|
-
body[data-page="thinking"] {
|
|
20
|
+
body[data-page="thinking"], body[data-page="goal-map"] {
|
|
18
21
|
color-scheme: dark;
|
|
19
22
|
--void: #000000; --ink: #ffffff; --ink-soft: #c0c0c0; --ink-faint: #a8a8a8; --ink-dim: #65656a; --ink-ghost: #3a3a3f;
|
|
20
23
|
--rule: rgba(255,255,255,.16); --rule-soft: rgba(255,255,255,.08); --glass: rgba(255,255,255,.075); --glass-strong: rgba(255,255,255,.14);
|
|
@@ -23,7 +26,7 @@ body[data-page="thinking"] {
|
|
|
23
26
|
--face: Manrope, "Segoe UI", system-ui, -apple-system, sans-serif;
|
|
24
27
|
}
|
|
25
28
|
html, body { height: 100%; }
|
|
26
|
-
body[data-page="thinking"] { margin: 0; background: var(--void); color: var(--ink); font-family: var(--face); font-size: 15px; line-height: 1.5; overflow: hidden; -webkit-font-smoothing: antialiased; }
|
|
29
|
+
body[data-page="thinking"], body[data-page="goal-map"] { margin: 0; background: var(--void); color: var(--ink); font-family: var(--face); font-size: 15px; line-height: 1.5; overflow: hidden; -webkit-font-smoothing: antialiased; }
|
|
27
30
|
button { font-family: inherit; }
|
|
28
31
|
a { color: var(--accent); }
|
|
29
32
|
.lbl { font: var(--label); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); }
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
links, injected into this head by the server like every hall page's
|
|
13
13
|
(tests/hall_tokens.mjs: no hall page hardcodes a web-font stylesheet).
|
|
14
14
|
The template carried its own Google Fonts link for the same two faces. -->
|
|
15
|
-
<link rel="stylesheet" href="/builders/thinking.css?v=2026-09-25-
|
|
15
|
+
<link rel="stylesheet" href="/builders/thinking.css?v=2026-09-25-sky4">
|
|
16
16
|
</head>
|
|
17
17
|
<!-- THE SKY (task 1004231 / sky part 3 of 5, goal 1000095; ADR 0331, ADR 0227
|
|
18
18
|
§11). This page is docs/design/mocks/sky/sky.template.html — the prototype
|
|
@@ -29,7 +29,13 @@
|
|
|
29
29
|
room survives inside it as the list drawer (L). /sky, the old ambient
|
|
30
30
|
page's URL, 301s here (src/bongos/serve-internal.js). It carries no hall
|
|
31
31
|
chrome on purpose: the sky is a full-screen surface with its own header,
|
|
32
|
-
and the hall's doors (the record, the goal, the task) open from the panel.
|
|
32
|
+
and the hall's doors (the record, the goal, the task) open from the panel.
|
|
33
|
+
|
|
34
|
+
The same markup, sheet and scripts serve THE PROJECT SKY at /goal-map
|
|
35
|
+
(part 4, task 1004232): that body says data-sky-lens="all", so the boot
|
|
36
|
+
opens in "the whole project" and the tour hands back to it. The switcher
|
|
37
|
+
is two doors — "the whole project" leaves for /goal-map from here, and
|
|
38
|
+
"simple" / "your thinking" leave for here from there. -->
|
|
33
39
|
<body data-page="thinking" data-page-title="Your sky">
|
|
34
40
|
|
|
35
41
|
<!-- The one <main> landmark the kit's a11y floor asks of every hall page
|
|
@@ -124,13 +130,13 @@
|
|
|
124
130
|
</div>
|
|
125
131
|
</footer>
|
|
126
132
|
|
|
127
|
-
<script src="/builders/idea-objects.js?v=2026-09-25-
|
|
128
|
-
<script src="/builders/sky-data.js?v=2026-09-25-
|
|
129
|
-
<script src="/builders/sky-layout.js?v=2026-09-25-
|
|
130
|
-
<script src="/builders/sky-draw.js?v=2026-09-25-
|
|
131
|
-
<script src="/builders/sky-interaction.js?v=2026-09-25-
|
|
132
|
-
<script src="/builders/sky-panel.js?v=2026-09-25-
|
|
133
|
-
<script src="/builders/sky-composer.js?v=2026-09-25-
|
|
134
|
-
<script src="/builders/sky-tour.js?v=2026-09-25-
|
|
133
|
+
<script src="/builders/idea-objects.js?v=2026-09-25-sky4"></script>
|
|
134
|
+
<script src="/builders/sky-data.js?v=2026-09-25-sky4"></script>
|
|
135
|
+
<script src="/builders/sky-layout.js?v=2026-09-25-sky4"></script>
|
|
136
|
+
<script src="/builders/sky-draw.js?v=2026-09-25-sky4"></script>
|
|
137
|
+
<script src="/builders/sky-interaction.js?v=2026-09-25-sky4"></script>
|
|
138
|
+
<script src="/builders/sky-panel.js?v=2026-09-25-sky4"></script>
|
|
139
|
+
<script src="/builders/sky-composer.js?v=2026-09-25-sky4"></script>
|
|
140
|
+
<script src="/builders/sky-tour.js?v=2026-09-25-sky4"></script>
|
|
135
141
|
</body>
|
|
136
142
|
</html>
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
|
|
163
163
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
164
164
|
chrome exists before the page script runs. -->
|
|
165
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
165
|
+
<script src="/builders/shell.js?v=2026-09-25-sky4b"></script>
|
|
166
166
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
167
167
|
<script src="/builders/palette.js" defer></script>
|
|
168
168
|
<!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
|
|
172
172
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
173
173
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
174
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
174
|
+
<script src="/builders/shell.js?v=2026-09-25-sky4b"></script>
|
|
175
175
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
176
176
|
<script src="/builders/palette.js" defer></script>
|
|
177
177
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/ideas/born-of.js — WHICH THOUGHTS BECAME THE STRUCTURE (task 1004233,
|
|
4
|
+
// sky part 5 of 5; goal 1000095; ADR 0331 §4).
|
|
5
|
+
//
|
|
6
|
+
// The sky draws a goal born of a thought with a nucleus ring, a ring (a
|
|
7
|
+
// criterion) born of one with an accent notch, and places the thought itself at
|
|
8
|
+
// the well's core or on its ring. The prototype found "born of" by scanning goal
|
|
9
|
+
// and criterion text for idea ids; this read answers it from the two tables the
|
|
10
|
+
// ratify paths already write, so it is a ledger fact and never a guess:
|
|
11
|
+
// goals — ideas_goal_ratifications (ratify.js ratifyIdeaAsGoal): one row
|
|
12
|
+
// per idea → goal
|
|
13
|
+
// criteria — criterion_proposal_audit (ratify.js ratifyCriterionProposal):
|
|
14
|
+
// one row per idea → criterion
|
|
15
|
+
// The FIRST ratification names the thought (ordered by ratified_at, then id);
|
|
16
|
+
// ids come back as strings keyed by the thing born, so a page looks a well or a
|
|
17
|
+
// ring up directly:
|
|
18
|
+
// { goals: { [goal_id]: idea_id }, criteria: { [criterion_id]: idea_id } }
|
|
19
|
+
//
|
|
20
|
+
// Served through the `ideas.bornOf` PORT (registered beside `ideas.capture` in
|
|
21
|
+
// routes/inbox.js) so the lifecycle module's GET /sky can fold it in as `born`
|
|
22
|
+
// without importing this module or reading an ideas_-prefixed table itself
|
|
23
|
+
// (ADR 0083's one-way rule). An instance without the port gets an empty map,
|
|
24
|
+
// which the pages draw as "nothing is known", not "nothing was born".
|
|
25
|
+
|
|
26
|
+
const api = require('../../src/module-api');
|
|
27
|
+
const { pool } = api;
|
|
28
|
+
|
|
29
|
+
function emptyBorn() { return { goals: {}, criteria: {} }; }
|
|
30
|
+
|
|
31
|
+
// readBornOf — the two reads, folded first-wins into plain objects keyed by the
|
|
32
|
+
// born thing's id. deps.pool honours the test seam.
|
|
33
|
+
async function readBornOf(deps = {}) {
|
|
34
|
+
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
35
|
+
const [goals, criteria] = await Promise.all([
|
|
36
|
+
activePool.query(
|
|
37
|
+
`SELECT goal_id::text AS goal_id, idea_id::text AS idea_id
|
|
38
|
+
FROM ideas_goal_ratifications
|
|
39
|
+
ORDER BY ratified_at, id`
|
|
40
|
+
),
|
|
41
|
+
activePool.query(
|
|
42
|
+
`SELECT criterion_id::text AS criterion_id, idea_id::text AS idea_id
|
|
43
|
+
FROM criterion_proposal_audit
|
|
44
|
+
ORDER BY ratified_at, id`
|
|
45
|
+
),
|
|
46
|
+
]);
|
|
47
|
+
const firstWins = (rows, key) => {
|
|
48
|
+
const m = new Map();
|
|
49
|
+
for (const r of rows) if (r && r[key] != null && r.idea_id != null && !m.has(String(r[key]))) m.set(String(r[key]), String(r.idea_id));
|
|
50
|
+
return Object.fromEntries(m);
|
|
51
|
+
};
|
|
52
|
+
return { goals: firstWins(goals.rows, 'goal_id'), criteria: firstWins(criteria.rows, 'criterion_id') };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { readBornOf, emptyBorn };
|
package/modules/ideas/inbox.js
CHANGED
|
@@ -32,6 +32,7 @@ const {
|
|
|
32
32
|
IDEA_COLS, IDEA_COLS_Q, GRADE_COLS_Q, IDEA_JOIN_GRADE, ungradedProjection,
|
|
33
33
|
DEFAULT_GRADE, VALID_GRADES, DEFAULT_TEMPLATE_KEY, TEMPLATE_KEY_MAX, FIELD_VALUES_MAX,
|
|
34
34
|
VALID_RECOMMENDATIONS, isValidRecommendation, normalizeRecommendation,
|
|
35
|
+
FILED_VIA_VALUES, isValidFiledVia, filedViaForSource, resolveFiledVia,
|
|
35
36
|
} = require('./projection');
|
|
36
37
|
// The carved domains. Each is re-exported at the bottom of this file, so every
|
|
37
38
|
// existing caller of `require('./inbox')` keeps working unchanged — the same
|
|
@@ -170,6 +171,13 @@ async function captureIdea({
|
|
|
170
171
|
// anything). Ignored when `goalId` is already set, so every existing caller
|
|
171
172
|
// is untouched.
|
|
172
173
|
suggestedGoalId = null,
|
|
174
|
+
// task 1004233 (sky part 5): WHICH DOOR the idea came through, as a ledger
|
|
175
|
+
// fact on the row — hall | cli | discord | planning | api (projection.js
|
|
176
|
+
// FILED_VIA_VALUES). The route reads it off the session; a port caller (the
|
|
177
|
+
// Discord bot, an agent poller) names its own. 'api' is the catch-all for a
|
|
178
|
+
// program that says nothing: a NEW row always records a door — NULL is
|
|
179
|
+
// reserved for rows older than the column and is never written here.
|
|
180
|
+
filedVia = 'api',
|
|
173
181
|
// WHICH DOOR this filing came through; absent is UNTRUSTED and stays an inbox
|
|
174
182
|
// row. Read by one rule — see routing.HOMELESS_ROUTING_VECTORS (ADR 0262).
|
|
175
183
|
vector = null,
|
|
@@ -187,6 +195,9 @@ async function captureIdea({
|
|
|
187
195
|
) {
|
|
188
196
|
throw { code: 'BAD_PRIORITY', priority: suggestedPriority };
|
|
189
197
|
}
|
|
198
|
+
if (!isValidFiledVia(filedVia)) {
|
|
199
|
+
throw { code: 'BAD_FILED_VIA', filedVia, allowed: [...FILED_VIA_VALUES] };
|
|
200
|
+
}
|
|
190
201
|
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
191
202
|
|
|
192
203
|
// The decision, before any write. Pure — see ./routing.js.
|
|
@@ -225,7 +236,7 @@ async function captureIdea({
|
|
|
225
236
|
if (landing.route) {
|
|
226
237
|
return routeIdeaToGoal({
|
|
227
238
|
title, bodyMd, resolvedKind, suggestedVersion, suggestedPriority,
|
|
228
|
-
capturedBy, goalId, landing, filerRank, touches,
|
|
239
|
+
capturedBy, goalId, landing, filerRank, touches, filedVia,
|
|
229
240
|
}, activePool);
|
|
230
241
|
}
|
|
231
242
|
|
|
@@ -236,12 +247,12 @@ async function captureIdea({
|
|
|
236
247
|
const { rows } = await activePool.query(
|
|
237
248
|
`INSERT INTO idea_inbox
|
|
238
249
|
(title, body_md, kind, suggested_version, suggested_priority, captured_by, status,
|
|
239
|
-
suggested_goal_id)
|
|
250
|
+
suggested_goal_id, filed_via)
|
|
240
251
|
VALUES ($1, $2, $3, $4, $5, $6, 'open',
|
|
241
|
-
$7)
|
|
252
|
+
$7, $8)
|
|
242
253
|
RETURNING ${IDEA_COLS}`,
|
|
243
254
|
[title, bodyMd, resolvedKind, suggestedVersion, suggestedPriority, capturedBy,
|
|
244
|
-
goalId ?? suggestedGoalId ?? null]
|
|
255
|
+
goalId ?? suggestedGoalId ?? null, filedVia]
|
|
245
256
|
);
|
|
246
257
|
// A brand-new idea has no ideas_grades row, so it is quick by definition —
|
|
247
258
|
// no join needed to know that. Grading it (R143's Full Idea capture) is a
|
|
@@ -266,7 +277,7 @@ async function captureIdea({
|
|
|
266
277
|
// the same two-step ratifyCriterionProposal takes.
|
|
267
278
|
async function routeIdeaToGoal({
|
|
268
279
|
title, bodyMd, resolvedKind, suggestedVersion, suggestedPriority,
|
|
269
|
-
capturedBy, goalId, landing, filerRank = null, touches = null,
|
|
280
|
+
capturedBy, goalId, landing, filerRank = null, touches = null, filedVia = 'api',
|
|
270
281
|
}, activePool) {
|
|
271
282
|
const client = await activePool.connect();
|
|
272
283
|
try {
|
|
@@ -326,12 +337,12 @@ async function routeIdeaToGoal({
|
|
|
326
337
|
const { rows: ideaRows } = await client.query(
|
|
327
338
|
`INSERT INTO idea_inbox
|
|
328
339
|
(title, body_md, kind, suggested_version, suggested_priority, captured_by, status,
|
|
329
|
-
suggested_goal_id, reviewed_at)
|
|
340
|
+
suggested_goal_id, reviewed_at, filed_via)
|
|
330
341
|
VALUES ($1, $2, $3, $4, $5, $6, 'promoted',
|
|
331
|
-
$7, now())
|
|
342
|
+
$7, now(), $8)
|
|
332
343
|
RETURNING ${IDEA_COLS}`,
|
|
333
344
|
[title, bodyMd, resolvedKind, suggestedVersion, suggestedPriority, capturedBy,
|
|
334
|
-
effectiveGoalId]
|
|
345
|
+
effectiveGoalId, filedVia]
|
|
335
346
|
);
|
|
336
347
|
const idea = ideaRows[0];
|
|
337
348
|
|
|
@@ -802,6 +813,10 @@ module.exports = {
|
|
|
802
813
|
// the same place the SQL CHECK mirrors
|
|
803
814
|
VALID_RECOMMENDATIONS,
|
|
804
815
|
isValidRecommendation,
|
|
816
|
+
FILED_VIA_VALUES,
|
|
817
|
+
isValidFiledVia,
|
|
818
|
+
filedViaForSource,
|
|
819
|
+
resolveFiledVia,
|
|
805
820
|
normalizeRecommendation,
|
|
806
821
|
// skill-friction marker + rank visibility gate (V3.R70 / #289; the kind
|
|
807
822
|
// itself landed in task 1003105 — both markers are live, see above)
|
|
@@ -25,6 +25,11 @@ const IDEA_COL_NAMES = [
|
|
|
25
25
|
'id', 'title', 'body_md', 'kind', 'suggested_version', 'suggested_priority',
|
|
26
26
|
'captured_at', 'captured_by', 'status', 'promoted_to_task_id', 'reviewed_at',
|
|
27
27
|
'merged_into_idea_id', 'triage_note',
|
|
28
|
+
// core_253_idea_filed_via (task 1004233, sky part 5): WHICH DOOR the idea came through
|
|
29
|
+
// (hall | cli | discord | planning | api), or NULL for a row older than the
|
|
30
|
+
// column. On the shared list so every read carries it — GET /sky/mine is the
|
|
31
|
+
// trail's own projection and the sky's "claude layer" reads it off the row.
|
|
32
|
+
'filed_via',
|
|
28
33
|
// core_222 (task 1003060, BV1.R02): the routing HINT — "this probably belongs
|
|
29
34
|
// to goal N" — on a row that did NOT convert. Added to the shared list because
|
|
30
35
|
// R02 shipped the column and nothing read it: /idea-triage (R13) cannot walk
|
|
@@ -34,6 +39,44 @@ const IDEA_COL_NAMES = [
|
|
|
34
39
|
'suggested_goal_id',
|
|
35
40
|
];
|
|
36
41
|
const IDEA_COLS = IDEA_COL_NAMES.join(', ');
|
|
42
|
+
|
|
43
|
+
// -------------------------------------------------------------------------
|
|
44
|
+
// Where an idea was filed from (task 1004233 / sky part 5, goal 1000095; ADR 0331 §4)
|
|
45
|
+
//
|
|
46
|
+
// The closed set idea_inbox_filed_via_known mirrors (core_253_idea_filed_via). NULL — a row
|
|
47
|
+
// older than the column — is a sixth, honest answer the sky speaks as "filed
|
|
48
|
+
// before the ledger recorded where"; it is never backfilled from text, which
|
|
49
|
+
// would put the prototype's guess into the ledger.
|
|
50
|
+
//
|
|
51
|
+
// THE DOOR IS READ OFF THE SESSION, not claimed in the body. A web cookie is the
|
|
52
|
+
// hall (the ideas form, the sky's composer), a device-flow token is the CLI
|
|
53
|
+
// (capture.js, spark.js), anything else (a box token, a program on the port) is
|
|
54
|
+
// the API — so a filer cannot say they came through a door they did not. The one
|
|
55
|
+
// value a body may ADD is 'planning': a planning session's capture is a CLI
|
|
56
|
+
// filing no session can tell apart, so capture.js --via planning names it. The
|
|
57
|
+
// Discord bot names 'discord' itself: it calls the port, not the route.
|
|
58
|
+
const FILED_VIA_VALUES = Object.freeze(['hall', 'cli', 'discord', 'planning', 'api']);
|
|
59
|
+
const isValidFiledVia = (v) => typeof v === 'string' && FILED_VIA_VALUES.includes(v);
|
|
60
|
+
function filedViaForSource(source) {
|
|
61
|
+
return source === 'web' ? 'hall' : source === 'cli' ? 'cli' : 'api';
|
|
62
|
+
}
|
|
63
|
+
// resolveFiledVia — the session's door, unless the body names 'planning' (or
|
|
64
|
+
// repeats the session's own door, which is harmless). Anything else is refused
|
|
65
|
+
// with the reason and the one value that may be named.
|
|
66
|
+
function resolveFiledVia(source, claimed) {
|
|
67
|
+
const own = filedViaForSource(source);
|
|
68
|
+
if (claimed === undefined || claimed === null || claimed === own) return { ok: true, value: own };
|
|
69
|
+
if (claimed === 'planning') return { ok: true, value: 'planning' };
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
detail: {
|
|
73
|
+
filed_via: claimed, session_door: own, allowed: ['planning', own],
|
|
74
|
+
reason: isValidFiledVia(claimed)
|
|
75
|
+
? `filed_via is read off your session (${own}); the only door a filing may add is 'planning'.`
|
|
76
|
+
: `filed_via must be one of ${FILED_VIA_VALUES.join(', ')} — and only 'planning' may be named by a filing.`,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
37
80
|
const IDEA_COLS_Q = IDEA_COL_NAMES.map((c) => `i.${c}`).join(', ');
|
|
38
81
|
|
|
39
82
|
// -------------------------------------------------------------------------
|
|
@@ -153,6 +196,11 @@ module.exports = {
|
|
|
153
196
|
VALID_RECOMMENDATIONS,
|
|
154
197
|
isValidRecommendation,
|
|
155
198
|
normalizeRecommendation,
|
|
199
|
+
FILED_VIA_VALUES,
|
|
200
|
+
isValidFiledVia,
|
|
201
|
+
filedViaForSource,
|
|
202
|
+
resolveFiledVia,
|
|
203
|
+
IDEA_COL_NAMES,
|
|
156
204
|
GRADE_COLS_Q,
|
|
157
205
|
IDEA_JOIN_GRADE,
|
|
158
206
|
ungradedProjection,
|