@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.
Files changed (104) hide show
  1. package/.bongos-core.json +145 -140
  2. package/.claude/skills/planning-session/SKILL.md +1 -1
  3. package/clients/bongos-client/index.d.ts +1 -1
  4. package/docs/adr/0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md +4 -1
  5. package/docs/adr/0331-the-sky-is-the-ideators-home.md +18 -1
  6. package/docs/adr/0335-an-invite-is-a-pre-approved-row-on-the-projects-own-instance.md +113 -0
  7. package/docs/adr/0336-a-guilds-record-is-the-sum-of-what-its-members-already-show.md +201 -0
  8. package/docs/adr/README.md +2 -0
  9. package/docs/api/openapi.json +4 -0
  10. package/docs/api-reference.md +1 -1
  11. package/docs/architecture.md +4 -1
  12. package/docs/copy-inventory.md +139 -110
  13. package/docs/copy-registry.json +872 -296
  14. package/docs/file-map.md +0 -1
  15. package/docs/module-api-changelog.md +4 -0
  16. package/migrations/core_253_idea_filed_via.sql +59 -0
  17. package/modules/agents/pollers/event-router.js +2 -0
  18. package/modules/discord/discord-bugs.js +2 -0
  19. package/modules/discord/discord-inbound.js +3 -0
  20. package/modules/hall-ui/public/atlas.html +1 -1
  21. package/modules/hall-ui/public/blockers.html +1 -1
  22. package/modules/hall-ui/public/board-room.html +1 -1
  23. package/modules/hall-ui/public/collab.html +1 -1
  24. package/modules/hall-ui/public/copy-desk.html +1 -1
  25. package/modules/hall-ui/public/deploy.html +1 -1
  26. package/modules/hall-ui/public/diagrams.html +1 -1
  27. package/modules/hall-ui/public/drachmae.html +1 -1
  28. package/modules/hall-ui/public/fleet.html +1 -1
  29. package/modules/hall-ui/public/gate.html +1 -1
  30. package/modules/hall-ui/public/goal-map.html +125 -52
  31. package/modules/hall-ui/public/goal-map.states.json +29 -5
  32. package/modules/hall-ui/public/goals.html +1 -1
  33. package/modules/hall-ui/public/government.html +1 -1
  34. package/modules/hall-ui/public/idea.html +1 -1
  35. package/modules/hall-ui/public/ideas.html +1 -1
  36. package/modules/hall-ui/public/index.html +1 -1
  37. package/modules/hall-ui/public/modules.html +1 -1
  38. package/modules/hall-ui/public/people.html +1 -1
  39. package/modules/hall-ui/public/primer.html +1 -1
  40. package/modules/hall-ui/public/profile.html +1 -1
  41. package/modules/hall-ui/public/project-settings.html +1 -1
  42. package/modules/hall-ui/public/ranks.html +1 -1
  43. package/modules/hall-ui/public/roadmap.html +1 -1
  44. package/modules/hall-ui/public/roster.html +1 -1
  45. package/modules/hall-ui/public/sessions.html +1 -1
  46. package/modules/hall-ui/public/settings.html +1 -1
  47. package/modules/hall-ui/public/shell.js +11 -0
  48. package/modules/hall-ui/public/sky-data.js +41 -9
  49. package/modules/hall-ui/public/sky-draw.js +3 -2
  50. package/modules/hall-ui/public/sky-interaction.js +14 -7
  51. package/modules/hall-ui/public/sky-layout.js +2 -1
  52. package/modules/hall-ui/public/sky-panel.js +1 -1
  53. package/modules/hall-ui/public/sky-tour.js +10 -6
  54. package/modules/hall-ui/public/studio.html +1 -1
  55. package/modules/hall-ui/public/task.html +1 -1
  56. package/modules/hall-ui/public/thinking.css +6 -3
  57. package/modules/hall-ui/public/thinking.html +16 -10
  58. package/modules/hall-ui/public/watch.html +1 -1
  59. package/modules/hall-ui/public/work.html +1 -1
  60. package/modules/ideas/born-of.js +55 -0
  61. package/modules/ideas/inbox.js +23 -8
  62. package/modules/ideas/projection.js +48 -0
  63. package/modules/ideas/routes/inbox.js +20 -0
  64. package/modules/lifecycle/sky.js +25 -4
  65. package/modules/platform-identity/activity-snapshots.js +47 -0
  66. package/modules/platform-identity/migrations/platform_identity_022_activity_snapshots.sql +46 -0
  67. package/modules/platform-identity/platform-identity.js +13 -0
  68. package/modules/platform-identity/tests/activity-snapshots.mjs +68 -0
  69. package/modules/platform-identity/tests/platform-identity.mjs +21 -0
  70. package/modules/ui-design/skills/impeccable/SKILL.md +1 -1
  71. package/package-lock.json +2 -2
  72. package/package.json +1 -1
  73. package/scripts/gds/capture.js +11 -0
  74. package/scripts/gds/run-unit-tests.js +4 -0
  75. package/src/bongos/serve-internal.js +4 -0
  76. package/src/module-api.js +1 -1
  77. package/tests/activity_snapshots_db.mjs +97 -0
  78. package/tests/discord_bugs.mjs +1 -0
  79. package/tests/discord_inbound.mjs +1 -0
  80. package/tests/goal_map_page.mjs +360 -211
  81. package/tests/guild_namespace_reserved.mjs +76 -0
  82. package/tests/hall_audit.mjs +1 -0
  83. package/tests/hall_nav.mjs +6 -1
  84. package/tests/hall_page_gate_map.mjs +3 -2
  85. package/tests/hall_sky.mjs +33 -16
  86. package/tests/helpers.mjs +21 -5
  87. package/tests/idea_filed_via.mjs +185 -0
  88. package/tests/landing_page.mjs +8 -0
  89. package/tests/nav_permission_atoms.mjs +1 -1
  90. package/tests/profile_rollup_consent.mjs +5 -1
  91. package/tests/project_admin_console_model.mjs +105 -0
  92. package/tests/render_check.mjs +6 -4
  93. package/tests/sky_read.mjs +19 -2
  94. package/tests/ui_design_skills.mjs +9 -0
  95. package/modules/hall-ui/public/goal-map-lib.js +0 -349
  96. package/modules/hall-ui/public/goal-map.css +0 -117
  97. package/modules/hall-ui/public/goal-map.js +0 -232
  98. package/modules/public-landing/public/world/assets/clouds/cloud-island-large.png +0 -0
  99. package/modules/public-landing/public/world/assets/clouds/cloud-puff.png +0 -0
  100. package/modules/public-landing/public/world/assets/creatures/creature-whale-cargo.png +0 -0
  101. package/modules/public-landing/public/world/assets/creatures/creature-whale-glide.png +0 -0
  102. package/modules/public-landing/public/world/assets/plates/plate-clouds-near.png +0 -0
  103. package/modules/public-landing/public/world/assets/plates/scene-establishing.png +0 -0
  104. 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-24-fleetcosts1"></script>
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-24-fleetcosts1"></script>
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-24-fleetcosts1"></script>
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 ARE HIDDEN UNTIL PART 5 makes them ledger facts (the README's
25
- // "must not ship" rows): the "claude layer" pill and every born-of mark. The
26
- // code that draws them is kept, and it reads `filed_via` / `bornOf` that are
27
- // simply absent today — so part 5 turns them on by shipping the facts.
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 is part 5's fact. Until it exists every thought is a person's
111
- // (the owner: historic ideas with no filed_via SHOW by default).
112
- filedVia: i.filed_via ?? null, origin: i.filed_via === 'session' ? 'claude' : 'human',
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 and the "filed by a session"
11
- // accent read facts part 5 ships; until then they are simply never true.
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 third lens button, "the whole
9
- // project", leaves for the project sky at /goal-map (part 4 builds that page)
10
- // instead of switching this one — the internal 'all' lens survives because the
11
- // tour walks it. And the "claude layer" pill only appears once part 5's
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
- $('lensSimple').addEventListener('click', () => setLens('simple')); $('lensMine').addEventListener('click', () => setLens('mine'));
94
- // "the whole project" is the project sky — its own page (part 4).
95
- $('lensAll').addEventListener('click', () => { location.assign(`${hallBase()}/goal-map`); });
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 (part 4 owns that page; the tour walks it here). The camera is a
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
- // Steps 7, 8 and 10 walk the project sky's lens ('all') inside this page —
11
- // which is why that lens survives here although its switcher button leaves
12
- // for /goal-map (part 4).
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('mine', true); SKY.clearCharts(); SKY.maybeStart(); S.dirty = true; }
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('mine', true); SKY.renderPanel(null); $('time').hidden = S.clockHidden;
72
- S.cam.k = SKY.fitRad(S.BANDS[0].r1 + 120);
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-24-fleetcosts1"></script>
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-24-fleetcosts1"></script>
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-sky3">
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-sky3"></script>
128
- <script src="/builders/sky-data.js?v=2026-09-25-sky3"></script>
129
- <script src="/builders/sky-layout.js?v=2026-09-25-sky3"></script>
130
- <script src="/builders/sky-draw.js?v=2026-09-25-sky3"></script>
131
- <script src="/builders/sky-interaction.js?v=2026-09-25-sky3"></script>
132
- <script src="/builders/sky-panel.js?v=2026-09-25-sky3"></script>
133
- <script src="/builders/sky-composer.js?v=2026-09-25-sky3"></script>
134
- <script src="/builders/sky-tour.js?v=2026-09-25-sky3"></script>
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-24-fleetcosts1"></script>
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-24-fleetcosts1"></script>
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 };
@@ -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,