@bongos/core 1.19.910 → 1.19.911

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.
@@ -109,8 +109,8 @@
109
109
  "modules/hall-ui/public/gate.js:157",
110
110
  "modules/hall-ui/public/hall-render.js:669",
111
111
  "modules/hall-ui/public/idea.js:207",
112
- "modules/hall-ui/public/ideas.js:183",
113
- "modules/hall-ui/public/ideas.js:561",
112
+ "modules/hall-ui/public/ideas.js:189",
113
+ "modules/hall-ui/public/ideas.js:666",
114
114
  "modules/hall-ui/public/watch.js:1242"
115
115
  ]
116
116
  },
@@ -812,7 +812,7 @@
812
812
  "occurrences": 2,
813
813
  "at": [
814
814
  "modules/hall-ui/public/blockers.js:311",
815
- "modules/hall-ui/public/ideas.html:107"
815
+ "modules/hall-ui/public/ideas.html:119"
816
816
  ]
817
817
  },
818
818
  {
@@ -1552,7 +1552,7 @@
1552
1552
  "occurrences": 2,
1553
1553
  "at": [
1554
1554
  "modules/hall-ui/public/idea.js:210",
1555
- "modules/hall-ui/public/ideas.js:433"
1555
+ "modules/hall-ui/public/ideas.js:439"
1556
1556
  ]
1557
1557
  },
1558
1558
  {
@@ -2043,7 +2043,7 @@
2043
2043
  "surface": "builders-hall",
2044
2044
  "text": "A Full Idea answers five questions, so that someone else can deliver against it without having to interview you first. Open to every role — the score below is the only filter, and it counts substance, not who you are.",
2045
2045
  "file": "modules/hall-ui/public/ideas.html",
2046
- "line": 115,
2046
+ "line": 127,
2047
2047
  "origin": "html-text",
2048
2048
  "confidence": "certain"
2049
2049
  },
@@ -2493,7 +2493,7 @@
2493
2493
  "surface": "builders-hall",
2494
2494
  "text": "Answer the five questions and this spark becomes a Full Idea.",
2495
2495
  "file": "modules/hall-ui/public/ideas.js",
2496
- "line": 692,
2496
+ "line": 805,
2497
2497
  "origin": "js-text-assign",
2498
2498
  "confidence": "certain"
2499
2499
  },
@@ -3753,7 +3753,7 @@
3753
3753
  "surface": "builders-hall",
3754
3754
  "text": "Could not load idea {…}.",
3755
3755
  "file": "modules/hall-ui/public/ideas.js",
3756
- "line": 735,
3756
+ "line": 848,
3757
3757
  "origin": "js-text-assign",
3758
3758
  "confidence": "certain"
3759
3759
  },
@@ -3807,7 +3807,7 @@
3807
3807
  "surface": "builders-hall",
3808
3808
  "text": "Could not load the idea template. Reload, or file from the terminal with capture.js.",
3809
3809
  "file": "modules/hall-ui/public/ideas.js",
3810
- "line": 717,
3810
+ "line": 830,
3811
3811
  "origin": "js-text-assign",
3812
3812
  "confidence": "certain"
3813
3813
  },
@@ -3834,7 +3834,7 @@
3834
3834
  "surface": "builders-hall",
3835
3835
  "text": "Could not load the queue just now.",
3836
3836
  "file": "modules/hall-ui/public/ideas.js",
3837
- "line": 656,
3837
+ "line": 769,
3838
3838
  "origin": "js-markup",
3839
3839
  "confidence": "certain"
3840
3840
  },
@@ -4275,7 +4275,7 @@
4275
4275
  "surface": "builders-hall",
4276
4276
  "text": "Counted, not judged: each question is worth the same, and earns its share in proportion to how much real text it carries. Whitespace and padding score nothing.",
4277
4277
  "file": "modules/hall-ui/public/ideas.html",
4278
- "line": 175,
4278
+ "line": 187,
4279
4279
  "origin": "html-text",
4280
4280
  "confidence": "certain"
4281
4281
  },
@@ -4419,7 +4419,7 @@
4419
4419
  "surface": "builders-hall",
4420
4420
  "text": "Develop this idea",
4421
4421
  "file": "modules/hall-ui/public/ideas.js",
4422
- "line": 691,
4422
+ "line": 804,
4423
4423
  "origin": "js-text-assign",
4424
4424
  "confidence": "certain"
4425
4425
  },
@@ -4428,7 +4428,7 @@
4428
4428
  "surface": "builders-hall",
4429
4429
  "text": "Developing a Big Idea Spark",
4430
4430
  "file": "modules/hall-ui/public/ideas.html",
4431
- "line": 127,
4431
+ "line": 139,
4432
4432
  "origin": "html-text",
4433
4433
  "confidence": "certain"
4434
4434
  },
@@ -4446,7 +4446,7 @@
4446
4446
  "surface": "builders-hall",
4447
4447
  "text": "Discard draft",
4448
4448
  "file": "modules/hall-ui/public/ideas.html",
4449
- "line": 153,
4449
+ "line": 165,
4450
4450
  "origin": "html-text",
4451
4451
  "confidence": "certain"
4452
4452
  },
@@ -4608,7 +4608,7 @@
4608
4608
  "surface": "builders-hall",
4609
4609
  "text": "Draft discarded.",
4610
4610
  "file": "modules/hall-ui/public/ideas.js",
4611
- "line": 446,
4611
+ "line": 452,
4612
4612
  "origin": "js-notice",
4613
4613
  "confidence": "certain"
4614
4614
  },
@@ -4869,7 +4869,7 @@
4869
4869
  "surface": "builders-hall",
4870
4870
  "text": "File it",
4871
4871
  "file": "modules/hall-ui/public/ideas.html",
4872
- "line": 107,
4872
+ "line": 119,
4873
4873
  "origin": "html-text",
4874
4874
  "confidence": "certain"
4875
4875
  },
@@ -4896,7 +4896,7 @@
4896
4896
  "surface": "builders-hall",
4897
4897
  "text": "File this idea",
4898
4898
  "file": "modules/hall-ui/public/ideas.html",
4899
- "line": 152,
4899
+ "line": 164,
4900
4900
  "origin": "html-text",
4901
4901
  "confidence": "certain"
4902
4902
  },
@@ -4914,7 +4914,7 @@
4914
4914
  "surface": "builders-hall",
4915
4915
  "text": "Filed to the inbox.",
4916
4916
  "file": "modules/hall-ui/public/ideas.js",
4917
- "line": 564,
4917
+ "line": 669,
4918
4918
  "origin": "js-text-assign",
4919
4919
  "confidence": "certain"
4920
4920
  },
@@ -5022,7 +5022,7 @@
5022
5022
  "surface": "builders-hall",
5023
5023
  "text": "Five questions, and a score that counts how much of each you actually answered.",
5024
5024
  "file": "modules/hall-ui/public/ideas.js",
5025
- "line": 727,
5025
+ "line": 840,
5026
5026
  "origin": "js-text-assign",
5027
5027
  "confidence": "certain"
5028
5028
  },
@@ -6246,7 +6246,7 @@
6246
6246
  "surface": "builders-hall",
6247
6247
  "text": "Loading the five questions…",
6248
6248
  "file": "modules/hall-ui/public/ideas.html",
6249
- "line": 146,
6249
+ "line": 158,
6250
6250
  "origin": "html-text",
6251
6251
  "confidence": "certain"
6252
6252
  },
@@ -6804,7 +6804,7 @@
6804
6804
  "surface": "builders-hall",
6805
6805
  "text": "No goal (homeless)",
6806
6806
  "file": "modules/hall-ui/public/ideas.js",
6807
- "line": 467,
6807
+ "line": 473,
6808
6808
  "origin": "js-markup",
6809
6809
  "confidence": "certain"
6810
6810
  },
@@ -7182,7 +7182,7 @@
7182
7182
  "surface": "builders-hall",
7183
7183
  "text": "Nothing is waiting. A spark appears here when its author offers a quick idea up for development.",
7184
7184
  "file": "modules/hall-ui/public/ideas.js",
7185
- "line": 634,
7185
+ "line": 747,
7186
7186
  "origin": "js-markup",
7187
7187
  "confidence": "certain"
7188
7188
  },
@@ -7272,7 +7272,7 @@
7272
7272
  "surface": "builders-hall",
7273
7273
  "text": "One line. This is what the idea is called everywhere else.",
7274
7274
  "file": "modules/hall-ui/public/ideas.html",
7275
- "line": 135,
7275
+ "line": 147,
7276
7276
  "origin": "html-text",
7277
7277
  "confidence": "certain"
7278
7278
  },
@@ -8136,7 +8136,7 @@
8136
8136
  "surface": "builders-hall",
8137
8137
  "text": "Recent filings",
8138
8138
  "file": "modules/hall-ui/public/ideas.html",
8139
- "line": 101,
8139
+ "line": 113,
8140
8140
  "origin": "html-text",
8141
8141
  "confidence": "certain"
8142
8142
  },
@@ -9009,7 +9009,7 @@
9009
9009
  "surface": "builders-hall",
9010
9010
  "text": "Showing {…} of {…}.",
9011
9011
  "file": "modules/hall-ui/public/ideas.js",
9012
- "line": 638,
9012
+ "line": 751,
9013
9013
  "origin": "js-markup",
9014
9014
  "confidence": "certain"
9015
9015
  },
@@ -11106,7 +11106,7 @@
11106
11106
  "surface": "builders-hall",
11107
11107
  "text": "Write a Full Idea",
11108
11108
  "file": "modules/hall-ui/public/ideas.html",
11109
- "line": 114,
11109
+ "line": 126,
11110
11110
  "origin": "html-text",
11111
11111
  "confidence": "certain"
11112
11112
  },
@@ -12069,7 +12069,7 @@
12069
12069
  "surface": "builders-hall",
12070
12070
  "text": "idea {…}",
12071
12071
  "file": "modules/hall-ui/public/ideas.js",
12072
- "line": 433,
12072
+ "line": 439,
12073
12073
  "origin": "js-markup",
12074
12074
  "confidence": "certain"
12075
12075
  },
@@ -12717,7 +12717,7 @@
12717
12717
  "surface": "builders-hall",
12718
12718
  "text": "task {…}",
12719
12719
  "file": "modules/hall-ui/public/ideas.js",
12720
- "line": 183,
12720
+ "line": 189,
12721
12721
  "origin": "js-markup",
12722
12722
  "confidence": "certain"
12723
12723
  },
@@ -12726,7 +12726,7 @@
12726
12726
  "surface": "builders-hall",
12727
12727
  "text": "task {…}",
12728
12728
  "file": "modules/hall-ui/public/ideas.js",
12729
- "line": 561,
12729
+ "line": 666,
12730
12730
  "origin": "js-markup",
12731
12731
  "confidence": "certain"
12732
12732
  },
@@ -12951,7 +12951,7 @@
12951
12951
  "surface": "builders-hall",
12952
12952
  "text": "what the idea is, in one line",
12953
12953
  "file": "modules/hall-ui/public/ideas.html",
12954
- "line": 138,
12954
+ "line": 150,
12955
12955
  "origin": "html-attr:placeholder",
12956
12956
  "confidence": "certain"
12957
12957
  },
@@ -2293,6 +2293,8 @@ is load-bearing: the script throws rather than guess if it is missing, and
2293
2293
  1.19.909 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2294
2294
  landed since 1.19.908 with no explicit bump. run 35517426559. (task 1002620)
2295
2295
  1.19.910 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2296
- landed since 1.19.909 with no explicit bump. run 35519713709. (task 1002620)
2296
+ landed since 1.19.909 with no explicit bump. run 35519966415. (task 1002620)
2297
+ 1.19.911 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2298
+ landed since 1.19.910 with no explicit bump. run 35520751450. (task 1002620)
2297
2299
  ---------------------------------------------------------------------------
2298
2300
  ```
@@ -71,6 +71,31 @@
71
71
 
72
72
  .fi-note { font-size: 13.5px; color: var(--ink-faint); }
73
73
 
74
+ /* GRAVITY (task 1004083 / spec R02) — the consequence of the picker above it,
75
+ so it sits tight under the select rather than at the form's own rhythm, and
76
+ reads a step louder than .fi-hint: a hint describes a control, this states
77
+ what will happen to the thought.
78
+
79
+ TONE IS THE STATUS VOCABULARY, the same .fi-fieldnote[data-ok] precedent, and
80
+ the three are three different facts rather than three severities:
81
+ lands — it becomes work. --ok, because that IS the verification colour:
82
+ the thought has a home and the system can name it.
83
+ floats — it waits in the inbox. NEUTRAL on purpose — floating is a legal
84
+ outcome the filer may have chosen, not a failure to warn about,
85
+ and painting it as one would push people to pick a goal they do
86
+ not believe in just to make the line go quiet.
87
+ refused — it will not be filed at all (the protected-scope wall). --danger,
88
+ because this one costs the filer the filing. */
89
+ .fi-gravity {
90
+ margin: 8px 0 0;
91
+ max-width: var(--measure);
92
+ font-size: 13.5px;
93
+ line-height: 1.5;
94
+ color: var(--ink-soft);
95
+ }
96
+ .fi-gravity[data-tone="lands"] { color: var(--ok); }
97
+ .fi-gravity[data-tone="refused"] { color: var(--danger); }
98
+
74
99
  .fi-err { margin: var(--sp-sm) 0 0; max-width: var(--measure); font-size: 14px; line-height: 1.5; color: var(--danger); }
75
100
  .fi-err[data-visible="0"] { display: none; }
76
101
 
@@ -89,6 +89,18 @@
89
89
  </label>
90
90
  <select id="fq-goal" class="fi-input"></select>
91
91
 
92
+ <!-- GRAVITY (task 1004083 / spec R02) — what will actually catch this
93
+ thought, stated before it is released. It sits under the picker
94
+ because it is the picker's consequence, and because the picker's
95
+ own option labels cannot carry it: "No goal (homeless)" is false
96
+ for a bug (ADR 0262 routes it to the maintenance goal), and a
97
+ named goal is false for a judgment kind (routing.js
98
+ NEVER_ROUTE_KINDS). The verdict comes from the server's real
99
+ landing matrix — see POST /inbox/preview-landing — never from a
100
+ second copy of the rules in ideas.js. Hidden until there is
101
+ something true to say. -->
102
+ <p class="fi-gravity" id="fq-gravity" role="status" aria-live="polite" hidden></p>
103
+
92
104
  <p class="fi-err" id="fq-err" role="alert"></p>
93
105
  <p class="fi-result" id="fq-result" role="status" aria-live="polite"></p>
94
106
 
@@ -88,6 +88,12 @@
88
88
  let goalTouchedByUser = false; // once the builder picks, stop clobbering it with fresh suggestions
89
89
  let quickSuggestTimer = null;
90
90
  let quickFiling = false;
91
+ // GRAVITY (task 1004083 / spec R02). The landing verdict for the draft as it
92
+ // currently stands, read from POST /inbox/preview-landing. A monotonic token
93
+ // guards against the out-of-order arrival a debounced-but-concurrent read
94
+ // invites: two previews in flight can land in either order, and the older one
95
+ // winning would leave the line describing a draft that no longer exists.
96
+ let gravitySeq = 0;
91
97
 
92
98
  // ---------------------------------------------------------------------------
93
99
  // draft persistence
@@ -501,9 +507,108 @@
501
507
  }
502
508
  }
503
509
 
510
+ // ---------------------------------------------------------------------------
511
+ // gravity — which goal will catch this thought, said before it is released
512
+ // (task 1004083 / spec R02)
513
+ // ---------------------------------------------------------------------------
514
+
515
+ const fqGravityEl = () => $('#fq-gravity');
516
+
517
+ // THE SENTENCES, AND NOTHING ELSE. Every branch below READS a field of the
518
+ // server's verdict; not one of them decides a landing. That split is the whole
519
+ // point of the endpoint: modules/ideas/routing.js `decideLanding` stays the one
520
+ // place the matrix lives, and a second copy of it in this file is precisely the
521
+ // failure the #fq-kind comment above already records ("this markup used to
522
+ // spell the whole vocabulary and had gone stale ... a third copy nobody was
523
+ // watching"). If a sentence here ever needs an `if` over kind or rank, the
524
+ // verdict is missing a field and the fix belongs on the server.
525
+ //
526
+ // Returns null for "say nothing" — an unscorable draft, or a read that failed.
527
+ // Silence is the honest degraded state: it leaves the filer exactly where they
528
+ // were before this feature, rather than replacing a quiet lie with a loud one.
529
+ function gravitySentence(v) {
530
+ if (!v || !v.outcome) return null;
531
+ const goalName = v.goal ? (v.goal.title || `goal ${v.goal.id}`) : null;
532
+ const claimable = v.status === 'ready';
533
+
534
+ if (v.outcome === 'refused') {
535
+ // The protected-scope wall. The server wrote the reason for a human; it is
536
+ // the one branch that already reads as a sentence, so it is passed through.
537
+ return { tone: 'refused', text: v.refusal && v.refusal.reason ? v.refusal.reason : `${goalName} will not take this filing.` };
538
+ }
539
+ if (v.outcome === 'routed') {
540
+ return {
541
+ tone: 'lands',
542
+ text: claimable
543
+ ? `This lands in ${goalName} as a task anyone can claim.`
544
+ : `This lands in ${goalName}, waiting for a nod before anyone can claim it.`,
545
+ };
546
+ }
547
+ if (v.outcome === 'routed_home') {
548
+ // The label on the picker says "homeless"; since ADR 0262 that is not true
549
+ // of a bug, and this line is the correction. Naming the version rather than
550
+ // the goal is deliberate — the maintenance goal is created by the capture,
551
+ // so the preview has no row to name without making one (landing-preview.js).
552
+ return {
553
+ tone: 'lands',
554
+ text: claimable
555
+ ? `A bug always lands. With no goal picked this goes to ${v.version_id}'s maintenance goal, claimable straight away.`
556
+ : `A bug always lands. With no goal picked this goes to ${v.version_id}'s maintenance goal, waiting for a nod.`,
557
+ };
558
+ }
559
+ // outcome === 'floats' — the inbox. Two ways to get here, and they are
560
+ // different facts: nobody named a goal, or a goal was named and the matrix
561
+ // declined it. The second is the one the picker currently lies about.
562
+ if (v.reason === 'judgment_kind') {
563
+ return { tone: 'floats', text: `${goalName} will not catch this — this kind is judged, not queued. It waits in the inbox for triage.` };
564
+ }
565
+ if (v.reason === 'full_idea_never_routes') {
566
+ return { tone: 'floats', text: 'A Full Idea is ratified by the board rather than routed. It waits in the inbox.' };
567
+ }
568
+ return { tone: 'floats', text: 'This floats in the inbox until someone triages it.' };
569
+ }
570
+
571
+ function renderGravity(v) {
572
+ const el = fqGravityEl();
573
+ if (!el) return;
574
+ const said = gravitySentence(v);
575
+ // dataset, not setAttribute: it writes the same data-tone the sheet selects
576
+ // on, and it is the half of the element a DOM the page is rendered into and
577
+ // a test stub both really carry.
578
+ if (!said) { el.hidden = true; el.textContent = ''; el.dataset.tone = ''; return; }
579
+ el.textContent = said.text;
580
+ el.dataset.tone = said.tone;
581
+ el.hidden = false;
582
+ }
583
+
584
+ // The read. Shares the suggester's debounce rather than adding a second clock:
585
+ // the two answer the same question at the same moment ("given what is typed so
586
+ // far, where does this go?"), and spec decision 12 — filing never gets slower —
587
+ // is a reason not to grow the number of things a keystroke sets running.
588
+ //
589
+ // A draft with no title is not previewed: it cannot be filed either, so there
590
+ // is no outcome to state.
591
+ async function refreshGravity() {
592
+ const seq = ++gravitySeq;
593
+ const title = fqTitleEl().value.trim();
594
+ if (!title) { renderGravity(null); return; }
595
+ const goalId = fqGoalEl().value;
596
+ const body = { kind: fqKindEl().value };
597
+ if (goalId) body.goal_id = Number(goalId);
598
+ let r;
599
+ try {
600
+ r = await api.inbox.postInboxPreviewLanding({ body });
601
+ } catch (_) {
602
+ r = null;
603
+ }
604
+ // A stale answer never paints. See gravitySeq above.
605
+ if (seq !== gravitySeq) return;
606
+ renderGravity(r && r.ok ? r.data : null);
607
+ }
608
+
504
609
  function onQuickTitleEdit() {
505
610
  clearTimeout(quickSuggestTimer);
506
- quickSuggestTimer = setTimeout(refreshGoalSuggestions, QUICK_SUGGEST_DEBOUNCE_MS);
611
+ quickSuggestTimer = setTimeout(() => { refreshGoalSuggestions(); refreshGravity(); }, QUICK_SUGGEST_DEBOUNCE_MS);
507
612
  }
508
613
 
509
614
  function showQuickErr(msg) {
@@ -570,6 +675,10 @@
570
675
  fqKindEl().value = 'bug';
571
676
  goalTouchedByUser = false;
572
677
  renderGoalOptions([]);
678
+ // The form is empty again, so the line has nothing to describe. The RECEIPT
679
+ // above is what survives the filing (task 1004082); gravity is a statement
680
+ // about a draft, and this draft is gone.
681
+ renderGravity(null);
573
682
  }
574
683
 
575
684
  async function bootQuickComposer() {
@@ -577,7 +686,11 @@
577
686
  renderGoalOptions([]);
578
687
  fqTitleEl().addEventListener('input', onQuickTitleEdit);
579
688
  fqKindEl().addEventListener('change', onQuickTitleEdit);
580
- fqGoalEl().addEventListener('change', () => { goalTouchedByUser = true; });
689
+ // Changing the goal is the one edit that changes gravity WITHOUT changing
690
+ // what the suggester would return, so it redraws the line directly rather
691
+ // than going through onQuickTitleEdit. Deliberately undebounced: a select is a
692
+ // deliberate single act, not a stream of keystrokes.
693
+ fqGoalEl().addEventListener('change', () => { goalTouchedByUser = true; refreshGravity(); });
581
694
  $('#fq-form').addEventListener('submit', fileQuick);
582
695
 
583
696
  // Before any network call: the receipts are local, so a builder who reloads
@@ -0,0 +1,180 @@
1
+ 'use strict';
2
+
3
+ // modules/ideas/landing-preview.js — THE LANDING VERDICT, BEFORE THE WRITE
4
+ // (task 1004083, BV2.R02 — "Gravity is visible before release").
5
+ //
6
+ // THE PROBLEM THIS ANSWERS. The composer's goal `<select>` is not invisible; it
7
+ // is visible and it MISLEADS, in two directions that the filer cannot see:
8
+ //
9
+ // 1. `No goal (homeless)` is FALSE FOR A BUG. Since ADR 0262 a homeless
10
+ // kind='bug' filed through the `api` vector routes into the version's
11
+ // maintenance goal (./homeless-home.js). The filer is told their report
12
+ // will float. It will not.
13
+ // 2. NAMING A GOAL IS FALSE FOR A JUDGMENT KIND. routing.NEVER_ROUTE_KINDS
14
+ // ('criterion-proposal', 'decision') and grade='full' never route, so the
15
+ // select shows a goal that will not catch the thought.
16
+ //
17
+ // So "gravity is visible" is not a restyle of the dropdown — the dropdown's own
18
+ // labels are the thing that is wrong. It is met by stating the matrix's verdict.
19
+ //
20
+ // NO NEW ROUTING, and this file is where that promise is kept. It DECIDES
21
+ // nothing: it calls the same `decideLanding` and `authorizeRoutedLanding` the
22
+ // capture calls, over the same inputs, and reports what they said. A second copy
23
+ // of the matrix in browser JS is the failure mode this exists to avoid — the
24
+ // same one the composer's `#fq-kind` comment already records ("this markup used
25
+ // to spell the whole vocabulary and had gone stale against the DB CHECK ... a
26
+ // third copy nobody was watching"), and the same one `ideas.js` already forbids
27
+ // in prose ("read it, never re-derive the landing matrix here").
28
+ //
29
+ // READ-ONLY IS A HARD CONTRACT, not an intention. Every statement below is a
30
+ // SELECT. In particular it calls `currentBuildingVersionId` and NEVER
31
+ // `ensureMaintenanceGoal` — that one is find-or-CREATE, and the preview runs on
32
+ // every keystroke of a debounced composer. Previewing a homeless bug must not
33
+ // mint a goal as a side effect of typing, so the preview names the version whose
34
+ // maintenance goal will catch it and leaves the row to the real capture.
35
+ //
36
+ // ITS OWN FILE for the reason homeless-home.js gives: inbox.js sits at the
37
+ // fitness line cap with no headroom, and "what will happen if I file this" is
38
+ // one question with one answer that a reader should find as a file.
39
+
40
+ const api = require('../../src/module-api');
41
+ const { pool } = api;
42
+ const { decideLanding, authorizeRoutedLanding } = require('./routing');
43
+
44
+ // The verdict vocabulary the composer renders. Stable machine tokens, never
45
+ // prose: the wording is the UI's business and the copy registry's, and a client
46
+ // that wants a different sentence must not have to string-match English.
47
+ //
48
+ // 'routed' — it becomes a task in a named goal. `status` says whether
49
+ // that task is claimable ('ready') or awaiting a nod
50
+ // ('backlog'); `goal` names the home.
51
+ // 'routed_home' — it becomes a task, but the goal is the MAINTENANCE goal the
52
+ // system picks, not one the filer named (the homeless bug).
53
+ // `version_id` names whose maintenance goal.
54
+ // 'floats' — it waits in the inbox for triage. The honest reading of
55
+ // "no goal", and also of a goal the matrix declined.
56
+ // 'refused' — it will not be filed at all (the protected-scope wall).
57
+ const OUTCOMES = Object.freeze({
58
+ ROUTED: 'routed',
59
+ ROUTED_HOME: 'routed_home',
60
+ FLOATS: 'floats',
61
+ REFUSED: 'refused',
62
+ });
63
+
64
+ // previewLanding — what pressing "File it" would do, without doing it.
65
+ //
66
+ // Takes exactly what the capture takes, and the caller resolves `filerRank` and
67
+ // `vector` the same way POST /inbox does: the rank off the AUTHENTICATED builder
68
+ // and the vector hardcoded. A preview that read either from the body would let a
69
+ // filer see — and then, by symmetry of argument, demand — a landing they do not
70
+ // hold. It is a read, so being wrong here leaks nothing; being wrong here
71
+ // TEACHES something false, which is the whole defect this task is about.
72
+ //
73
+ // NEVER THROWS. A composer redrawing on every keystroke must degrade to "we
74
+ // cannot say yet", never to a red error under a half-typed title. An outage in
75
+ // any read below collapses to `{ outcome: null }` and the UI says nothing —
76
+ // which leaves today's behaviour exactly as it is, rather than replacing a
77
+ // silent lie with a loud one.
78
+ async function previewLanding(input, deps = {}) {
79
+ // NORMALISE FIRST, DESTRUCTURE AFTER — the trap ./routing.js's `decideLanding`
80
+ // already records, and which the first cut of this function walked straight
81
+ // into. A default in the parameter list fires on `undefined` and NOT on
82
+ // `null`, so `previewLanding(null)` threw from the signature — BEFORE the
83
+ // try/catch below, so the "never throws" contract above was false for the one
84
+ // input a caller spreading an absent object actually produces.
85
+ const { goalId = null, kind = null, grade = null, filerRank = null, vector = null } = input || {};
86
+ const activePool = ((deps || {}).pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
87
+ try {
88
+ const landing = decideLanding({ goalId, kind, grade, filerRank, vector });
89
+
90
+ if (!landing.route) {
91
+ // A goal was named and the matrix declined it — the second lie above. The
92
+ // reason token travels so the UI can say WHY it still floats rather than
93
+ // showing a goal that is not going to catch anything.
94
+ return { outcome: OUTCOMES.FLOATS, reason: landing.reason, status: null, goal: null, version_id: null, refusal: null };
95
+ }
96
+
97
+ if (landing.homeless) {
98
+ // The first lie above. Read-only: name the version, never make the goal.
99
+ const lifecycle = api.resolveOptional && api.resolveOptional('lifecycle');
100
+ const versionId = lifecycle && lifecycle.currentBuildingVersionId
101
+ ? await lifecycle.currentBuildingVersionId()
102
+ : null;
103
+ // No building version means the real capture would fail closed
104
+ // (homeless-home.js NO_BUILDING_VERSION), so the preview must not promise
105
+ // a landing. It says nothing rather than naming a home that isn't there.
106
+ if (!versionId) return { outcome: null, reason: landing.reason, status: null, goal: null, version_id: null, refusal: null };
107
+ return {
108
+ outcome: OUTCOMES.ROUTED_HOME,
109
+ reason: landing.reason,
110
+ status: landing.status,
111
+ goal: null,
112
+ version_id: versionId,
113
+ refusal: null,
114
+ };
115
+ }
116
+
117
+ // A named goal. The same three facts the transaction checks — the goal is
118
+ // real, open, and on a building version — read WITHOUT the FOR SHARE lock,
119
+ // because a preview holds nothing and races nothing: it describes the world
120
+ // a moment before the write, and the write re-checks under its own lock.
121
+ const { rows } = await activePool.query(
122
+ `SELECT g.id, g.title, g.status, g.version_id, g.scope_modules, v.status AS version_status
123
+ FROM goals g
124
+ JOIN versions v ON v.id = g.version_id
125
+ WHERE g.id = $1`,
126
+ [goalId]
127
+ );
128
+ const goal = rows[0];
129
+ // A goal that is gone, archived, or on a closed version would be refused by
130
+ // the capture. The preview declines to guess a sentence for a state the
131
+ // filer cannot reach from the picker anyway (it lists open goals only), and
132
+ // lets the real 400 name it if they get there by other means.
133
+ if (!goal || goal.status !== 'open' || goal.version_status !== 'building') {
134
+ return { outcome: null, reason: landing.reason, status: null, goal: null, version_id: null, refusal: null };
135
+ }
136
+
137
+ // The binding gate, verbatim. `touches` is never declared by the composer —
138
+ // a person reporting a bug does not know which files it lives in — so the
139
+ // scope wall has nothing to subset-check and `scopeCheck` is null, exactly
140
+ // as it is for a human filing through POST /inbox. What CAN bite is the
141
+ // protected-scope refusal, and that is a server-only fact: it is the reason
142
+ // this preview cannot live in the browser.
143
+ const authorized = authorizeRoutedLanding({
144
+ landing,
145
+ filerRank,
146
+ scopeProtected: api.moduleScopeMap.scopeIncludesProtected(goal.scope_modules),
147
+ scopeCheck: null,
148
+ });
149
+ // The refusal NAMES the goal, and that is checked rather than assumed: the
150
+ // composer's own picker is filled from `GET /goals?status=open`, which applies
151
+ // no rank filter, so every open goal's title is already on screen for this
152
+ // caller — they picked this one FROM that list. Withholding it here would hide
153
+ // nothing and would cost the refusal its subject. If that read ever becomes
154
+ // rank-scoped, this line has to be revisited with it.
155
+ if (!authorized.ok) {
156
+ return {
157
+ outcome: OUTCOMES.REFUSED,
158
+ reason: landing.reason,
159
+ status: null,
160
+ goal: { id: goal.id, title: goal.title },
161
+ version_id: goal.version_id,
162
+ refusal: { code: authorized.code, reason: authorized.reason },
163
+ };
164
+ }
165
+
166
+ return {
167
+ outcome: OUTCOMES.ROUTED,
168
+ reason: landing.reason,
169
+ status: authorized.status,
170
+ goal: { id: goal.id, title: goal.title },
171
+ version_id: goal.version_id,
172
+ refusal: null,
173
+ };
174
+ } catch (_) {
175
+ // See the NEVER THROWS note above.
176
+ return { outcome: null, reason: null, status: null, goal: null, version_id: null, refusal: null };
177
+ }
178
+ }
179
+
180
+ module.exports = { previewLanding, OUTCOMES };