@ok-ggwp/git-gloss 0.2.0 → 0.4.0

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/dist/html/data.js CHANGED
@@ -2,8 +2,10 @@ import { commitDiff, commitSubject, git } from '../git.js';
2
2
  import { reviewStatus, statusPhrase, verdict } from '../reviews.js';
3
3
  import { parseDiff } from '../diff.js';
4
4
  import { loadGloss } from '../loader.js';
5
+ import { belowFloor } from '../render.js';
6
+ import { showsImpact, tryAnalyze } from '../floor.js';
5
7
  import { bodyLines, changedCount, gapText, isGap, refLabel, slice, sliceStats } from '../selection.js';
6
- import { NECESSITY, NECESSITY_HELP, servesOf } from '../schema.js';
8
+ import { IMPACT, IMPACT_HELP, NECESSITY, NECESSITY_HELP, servesOf } from '../schema.js';
7
9
  function pageReviews(sha) {
8
10
  return reviewStatus(sha).map((s) => ({
9
11
  stage: s.stage,
@@ -57,10 +59,23 @@ function pageHunk(h, lines, status, alsoIn) {
57
59
  lines: pageLines(h, lines),
58
60
  };
59
61
  }
62
+ // The order a reviewer should read a commit's groups in. Deterministic: a
63
+ // stable sort over the groups as they come, so ties keep their order.
64
+ export function reviewOrder(groups, focus = []) {
65
+ const rank = (g) => IMPACT.indexOf((g.impact ?? g.impact_derived)) + 1; // 0: unrated
66
+ const first = (g) => (focus.includes(g.id) ? focus.indexOf(g.id) : focus.length);
67
+ return groups
68
+ .map((g, i) => ({ g, i }))
69
+ .sort((a, b) => first(a.g) - first(b.g) || rank(b.g) - rank(a.g) || (b.g.added + b.g.removed) - (a.g.added + a.g.removed) || a.i - b.i)
70
+ .map((x) => x.g.id);
71
+ }
60
72
  const allLines = (h) => Array.from({ length: changedCount(h) }, (_, i) => i + 1);
61
73
  export function commitData(gloss) {
62
74
  const [author, date] = git(['log', '-1', '--format=%an%x00%aI', gloss.sha]).trim().split('\0');
63
75
  const a = gloss.note.annotation;
76
+ const an = tryAnalyze(gloss);
77
+ // Same threshold as `show`: derived impact badges only where the diff puts a group above module.
78
+ const derivedImpact = showsImpact(gloss.groups.map((x) => x.group), an);
64
79
  // Which groups claim each changed line, so shared lines are split between
65
80
  // their groups in the bar and each hunk can link to the groups it shares
66
81
  // lines with. A header-only hunk counts as one shared unit.
@@ -85,6 +100,9 @@ export function commitData(gloss) {
85
100
  id: g.id,
86
101
  intent: g.intent,
87
102
  necessity: g.necessity,
103
+ impact: g.impact,
104
+ impact_reason: g.impact_reason,
105
+ impact_derived: g.impact || !derivedImpact ? undefined : an?.groups.get(g.id)?.floor,
88
106
  serves: servesOf(g),
89
107
  removal_impact: g.removal_impact,
90
108
  evidence: g.evidence,
@@ -108,8 +126,11 @@ export function commitData(gloss) {
108
126
  glossed: true,
109
127
  task: a.task,
110
128
  summary: a.summary,
129
+ attention: a.attention ?? null,
130
+ computed: an ? { floor: an.floor, derived: an.derived, below: belowFloor(a.attention, an) } : null,
111
131
  warnings: gloss.warnings,
112
132
  groups,
133
+ order: reviewOrder(groups, a.attention?.focus),
113
134
  unannotated: gloss.unannotated.map((u) => pageHunk(u.hunk, u.lines, 'unannotated', [])),
114
135
  alternatives: a.alternatives ?? [],
115
136
  risks: a.risks ?? [],
@@ -125,8 +146,11 @@ function unglossedData(sha) {
125
146
  date,
126
147
  glossed: false,
127
148
  task: '',
149
+ attention: null,
150
+ computed: null,
128
151
  warnings: [],
129
152
  groups: [],
153
+ order: [],
130
154
  unannotated: parseDiff(commitDiff(sha)).map((h) => pageHunk(h, null, 'unannotated', [])),
131
155
  alternatives: [],
132
156
  risks: [],
@@ -138,6 +162,7 @@ export function buildPageData(shas, range = null, merges = []) {
138
162
  range,
139
163
  merges: merges.map((sha) => ({ sha, subject: commitSubject(sha) })),
140
164
  necessity: NECESSITY.map((name) => ({ name, help: NECESSITY_HELP[name] })),
165
+ impact: IMPACT.map((name) => ({ name, help: IMPACT_HELP[name] })),
141
166
  commits: shas.map((sha) => {
142
167
  const gloss = loadGloss(sha);
143
168
  return gloss ? commitData(gloss) : unglossedData(sha);
package/dist/html/page.js CHANGED
@@ -6,7 +6,7 @@ const asset = (name) => readFileSync(new URL(name, import.meta.url), 'utf8');
6
6
  export function embedJson(data) {
7
7
  return JSON.stringify(data).replace(/</g, '\\u003c');
8
8
  }
9
- const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
9
+ export const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
10
10
  export function renderPage(data) {
11
11
  // The page only runs our own inline script. If escaping ever failed, an
12
12
  // injected script would lack the nonce and the CSP would block it.
@@ -1,19 +1,24 @@
1
+ /* Review desk: a rail of groups (or commits) and a detail pane. System fonts
2
+ only: the page is one file and its CSP allows no other source. */
1
3
  :root {
2
- --bg: #f7f7f5;
3
- --surface: #ffffff;
4
- --surface-2: #f1f1ee;
5
- --border: #e2e2dc;
6
- --ok: #1a7f37;
7
- --text: #1d1d1b;
8
- --muted: #6b6b66;
9
- --accent: #3b5bdb;
4
+ --bg: #f3f4f6;
5
+ --panel: #ffffff;
6
+ --ink: #14171f;
7
+ --muted: #586073;
8
+ --line: #dde0e6;
9
+ --accent: #3446d6;
10
+ --accent-soft: #e7eafb;
11
+ --accent-strong: #2536a8;
12
+ --on-accent: #ffffff;
13
+ --ok: #1b6b35;
14
+ --warn: #a5530a;
10
15
  --warn-bg: #fff4e5;
11
16
  --warn-border: #f0b35b;
12
17
  --warn-text: #7a4300;
13
- --add-bg: #e6f6ea;
14
- --add-mark: #1a7f37;
15
- --del-bg: #fcebec;
16
- --del-mark: #c62a2f;
18
+ --add-bg: #e3f5e8;
19
+ --add-mark: #1b6b35;
20
+ --del-bg: #fbe7e8;
21
+ --del-mark: #b3262b;
17
22
  --meta-bg: #eef1f8;
18
23
  --meta-text: #4a5a85;
19
24
 
@@ -24,6 +29,19 @@
24
29
  --n-speculative: #9c36b5;
25
30
  --n-mechanical: #868e96;
26
31
 
32
+ /* Impact meter and attention gauge: one hue per step, and the steps differ
33
+ in lightness too, so they read without colour. */
34
+ --impact-local: #5b6373;
35
+ --impact-module: #0b6e83;
36
+ --impact-shared: #a5530a;
37
+ --impact-contract: #6b46c1;
38
+ --impact-sensitive: #b3262b;
39
+ --attention-skim: #5b6373;
40
+ --attention-look: #0b6e83;
41
+ --attention-careful: #a5530a;
42
+ --attention-must-read: #b3262b;
43
+ --attention-none: #586073;
44
+
27
45
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
28
46
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
29
47
  color-scheme: light;
@@ -31,21 +49,24 @@
31
49
 
32
50
  @media (prefers-color-scheme: dark) {
33
51
  :root {
34
- --bg: #151514;
35
- --surface: #1e1e1c;
36
- --surface-2: #262624;
37
- --border: #34342f;
38
- --ok: #57c47a;
39
- --text: #e8e8e3;
40
- --muted: #9a9a92;
41
- --accent: #8ea2ff;
52
+ --bg: #101218;
53
+ --panel: #181b23;
54
+ --ink: #e8eaf0;
55
+ --muted: #a0a7b6;
56
+ --line: #2b303c;
57
+ --accent: #8fa0ff;
58
+ --accent-soft: #232a4a;
59
+ --accent-strong: #b3bfff;
60
+ --on-accent: #101218;
61
+ --ok: #62d089;
62
+ --warn: #f0a24a;
42
63
  --warn-bg: #33270f;
43
64
  --warn-border: #a2701e;
44
65
  --warn-text: #f3c77a;
45
- --add-bg: #15301e;
46
- --add-mark: #5ccf7f;
47
- --del-bg: #3a1a1c;
48
- --del-mark: #ff7b80;
66
+ --add-bg: #14301d;
67
+ --add-mark: #62d089;
68
+ --del-bg: #3a1b1f;
69
+ --del-mark: #ff8085;
49
70
  --meta-bg: #20263a;
50
71
  --meta-text: #a3b1dc;
51
72
 
@@ -55,143 +76,183 @@
55
76
  --n-incidental: #e0a43e;
56
77
  --n-speculative: #cf7ee4;
57
78
  --n-mechanical: #8d949b;
79
+
80
+ --impact-local: #a3aab8;
81
+ --impact-module: #4cc3d9;
82
+ --impact-shared: #f0a24a;
83
+ --impact-contract: #b39dff;
84
+ --impact-sensitive: #ff8085;
85
+ --attention-skim: #a3aab8;
86
+ --attention-look: #4cc3d9;
87
+ --attention-careful: #f0a24a;
88
+ --attention-must-read: #ff8085;
89
+ --attention-none: #a0a7b6;
58
90
  color-scheme: dark;
59
91
  }
60
92
  }
61
93
 
62
94
  * { box-sizing: border-box; }
63
95
  html { -webkit-text-size-adjust: 100%; }
64
- body {
65
- margin: 0;
66
- background: var(--bg);
67
- color: var(--text);
68
- font: 15px/1.55 var(--sans);
69
- }
70
- main { min-width: 0; }
96
+ body { margin: 0; background: var(--bg); color: var(--ink); font: 14px/1.5 var(--sans); }
71
97
  a { color: var(--accent); text-decoration: none; }
72
98
  a:hover { text-decoration: underline; }
73
- code, .mono { font-family: var(--mono); font-size: 0.86em; }
74
-
75
- .layout {
76
- display: grid;
77
- /* minmax(0, …) so a wide diff scrolls inside its box instead of widening the page */
78
- grid-template-columns: minmax(0, 1fr);
79
- max-width: 1280px;
80
- margin: 0 auto;
81
- padding: 24px 16px 64px;
82
- gap: 32px;
83
- }
84
- @media (min-width: 1100px) {
85
- .layout { grid-template-columns: 240px minmax(0, 1fr); padding: 32px 32px 80px; }
86
- }
99
+ button { font: inherit; color: inherit; cursor: pointer; }
100
+ button:disabled { cursor: default; opacity: 0.5; }
101
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
102
+ code, .mono { font-family: var(--mono); font-size: 12px; }
103
+ .eyebrow { font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
104
+ .muted { color: var(--muted); }
105
+ kbd { font-family: var(--mono); font-size: 11px; border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 4px; padding: 0 5px; background: var(--panel); }
87
106
 
88
- /* Sidebar */
89
- .toc { display: none; }
90
- @media (min-width: 1100px) {
91
- .toc { display: block; position: sticky; top: 24px; align-self: start; max-height: calc(100vh - 48px); overflow: auto; }
92
- }
93
- .toc h2 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin: 0 0 8px; }
94
- .toc ol { list-style: none; margin: 0 0 24px; padding: 0; }
95
- .toc li a {
96
- display: flex; align-items: center; gap: 8px;
97
- padding: 4px 8px; border-radius: 6px; color: var(--text);
98
- font-family: var(--mono); font-size: 12.5px;
99
- }
100
- .toc li a:hover { background: var(--surface-2); text-decoration: none; }
101
- .toc li.hidden a { opacity: 0.35; }
102
- .toc li.active a { background: var(--surface-2); }
103
-
104
- .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; background: var(--c); }
105
-
106
- /* Header */
107
- .commit { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; color: var(--muted); font-size: 13px; }
108
- .sha { font-family: var(--mono); background: var(--surface-2); border: 1px solid var(--border); padding: 1px 6px; border-radius: 5px; color: var(--text); }
109
- h1 { font-size: 24px; line-height: 1.3; margin: 8px 0 16px; letter-spacing: -0.01em; }
110
- .statement { margin: 0 0 8px; max-width: 75ch; }
111
- .statement .label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; margin-right: 6px; }
112
-
113
- .warning {
114
- margin: 16px 0; padding: 12px 14px; border-radius: 8px;
115
- background: var(--warn-bg); border: 1px solid var(--warn-border); color: var(--warn-text);
116
- }
107
+ .desk { min-height: 100vh; display: flex; flex-direction: column; }
117
108
 
118
- /* Necessity overview */
119
- .overview { margin: 24px 0 8px; }
120
- .bar { display: flex; height: 10px; border-radius: 5px; overflow: hidden; background: var(--surface-2); }
121
- .bar span { display: block; height: 100%; background: var(--c); min-width: 2px; }
122
- .bar span + span { border-left: 2px solid var(--bg); }
123
- .chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
124
- .chip {
125
- display: inline-flex; align-items: center; gap: 6px;
126
- border: 1px solid var(--border); background: var(--surface); color: var(--text);
127
- border-radius: 999px; padding: 4px 10px; font: inherit; font-size: 13px; cursor: pointer;
109
+ /* Top bar */
110
+ .top { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 20px; padding: 10px 16px; background: var(--panel); border-bottom: 1px solid var(--line); }
111
+ .brand { font-weight: 600; letter-spacing: -0.01em; }
112
+ .sha { font-family: var(--mono); font-size: 12px; background: var(--bg); border: 1px solid var(--line); border-radius: 5px; padding: 1px 6px; color: var(--ink); }
113
+ .subject { flex: 1 1 260px; min-width: 0; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
114
+ .seg { display: flex; flex-wrap: wrap; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--bg); }
115
+ .seg button { border: 0; background: transparent; padding: 6px 12px; min-height: 44px; color: var(--muted); }
116
+ .seg button[aria-pressed="true"] { background: var(--ink); color: var(--panel); font-weight: 500; }
117
+ .seg .n { opacity: 0.75; margin-left: 4px; font-variant-numeric: tabular-nums; }
118
+ .prog { display: flex; align-items: center; gap: 10px; color: var(--muted); font-size: 13px; }
119
+ .track { width: 120px; height: 6px; border-radius: 3px; background: var(--line); overflow: hidden; }
120
+ .track i { display: block; height: 100%; width: 0; background: var(--accent); }
121
+
122
+ /* Body: rail above the pane when narrow, beside it from 900px */
123
+ .body { flex: 1; display: grid; grid-template-columns: minmax(0, 1fr); align-items: start; }
124
+ @media (min-width: 900px) {
125
+ /* the rail's panel colour runs the full height, however tall the top bar is */
126
+ .body { grid-template-columns: 360px minmax(0, 1fr); background: linear-gradient(90deg, var(--panel) 359px, var(--line) 359px 360px, transparent 360px); }
127
+ .body.range { grid-template-columns: 400px minmax(0, 1fr); background: linear-gradient(90deg, var(--panel) 399px, var(--line) 399px 400px, transparent 400px); }
128
+ .rail { position: sticky; top: 0; max-height: 100vh; overflow: auto; overscroll-behavior: contain; border-bottom: 0; border-right: 1px solid var(--line); }
128
129
  }
130
+ .rail { background: var(--panel); border-bottom: 1px solid var(--line); display: flex; flex-direction: column; min-width: 0; }
131
+ .main { min-width: 0; padding: 20px 16px 48px; }
132
+ @media (min-width: 900px) { .main { padding: 28px 40px 48px; } }
133
+
134
+ /* Attention: the card in the rail, the large one in a commit overview */
135
+ .att { margin: 20px 0 0; padding: 16px 18px; border: 1.5px solid var(--lv); border-radius: 12px; background: var(--panel); max-width: 720px; --lv: var(--attention-none); }
136
+ .att .lvl { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 22px; font-weight: 600; letter-spacing: 0.02em; color: var(--lv); }
137
+ .att .src { font-size: 12px; font-weight: 500; letter-spacing: 0; color: var(--muted); border: 1px solid var(--line); border-radius: 999px; padding: 0 8px; }
138
+ .att ul { margin: 10px 0 0; padding-left: 20px; }
139
+ .att li { margin-bottom: 4px; }
140
+ .att .look { margin: 10px 0 0; font-size: 13px; }
141
+ .att .override { margin: 10px 0 0; font-size: 13px; color: var(--warn-text); background: var(--warn-bg); border-radius: 6px; padding: 6px 10px; }
142
+ .att .override.mismatch { font-weight: 600; border: 1px solid var(--warn-border); }
143
+ .gauge { display: inline-flex; gap: 3px; }
144
+ .gauge i { display: block; width: 22px; height: 8px; border-radius: 2px; background: var(--line); }
145
+ .gauge i.on { background: var(--lv); }
146
+ .gauge.small i { width: 12px; height: 6px; }
147
+
148
+ /* Impact meter: five bars that grow, filled up to the level */
149
+ .meter { display: inline-flex; align-items: flex-end; gap: 2px; height: 16px; flex: none; }
150
+ .meter i { display: block; width: 4px; border-radius: 1px; background: var(--line); }
151
+ .meter i.on { background: var(--m, var(--impact-local)); }
152
+ .meter i:nth-child(1) { height: 6px; }
153
+ .meter i:nth-child(2) { height: 9px; }
154
+ .meter i:nth-child(3) { height: 11px; }
155
+ .meter i:nth-child(4) { height: 14px; }
156
+ .meter i:nth-child(5) { height: 16px; }
157
+ .meter.unrated i { background: transparent; border: 1px dashed var(--muted); }
158
+
159
+ /* Rail: groups */
160
+ .rail h2 { margin: 0; }
161
+ .railhead { padding: 4px 24px 8px; }
162
+ .necs { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 16px 10px; }
163
+ .chip { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--line); border-radius: 999px; padding: 2px 10px; font-size: 13px; background: var(--panel); color: var(--ink); }
164
+ button.chip { min-height: 32px; }
129
165
  .chip .count { color: var(--muted); font-variant-numeric: tabular-nums; }
130
- .chip[aria-pressed="false"] { opacity: 0.45; }
131
- .chip[aria-pressed="false"] .dot { background: transparent; border: 1.5px solid var(--c); }
132
- .overview .note { color: var(--muted); font-size: 12.5px; margin-top: 8px; }
133
-
134
- .toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; margin: 24px 0 12px; }
135
- .toolbar .shown { color: var(--muted); font-size: 13px; }
136
- .btn {
137
- border: 1px solid var(--border); background: var(--surface); color: var(--text);
138
- border-radius: 6px; padding: 4px 10px; font: inherit; font-size: 13px; cursor: pointer;
139
- }
140
- .btn:hover { background: var(--surface-2); }
166
+ .chip[aria-pressed="false"] { opacity: 0.6; text-decoration: line-through; }
167
+ .chip.link { cursor: pointer; }
168
+ .chip.link:hover { background: var(--accent-soft); }
169
+ .rows { display: flex; flex-direction: column; padding: 0 8px; }
170
+ .row { display: flex; align-items: stretch; border-radius: 8px; border: 1px solid transparent; }
171
+ .row.sel { background: var(--accent-soft); border-color: var(--accent); }
172
+ .row.done .rtitle { color: var(--muted); }
173
+ .check { flex: 0 0 44px; min-height: 44px; border: 0; background: transparent; display: flex; align-items: center; justify-content: center; }
174
+ .check span { width: 18px; height: 18px; border: 1.5px solid var(--muted); border-radius: 5px; display: flex; align-items: center; justify-content: center; background: var(--panel); font-size: 13px; line-height: 1; }
175
+ .check[aria-pressed="true"] span { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
176
+ .pick { flex: 1; min-width: 0; border: 0; background: transparent; text-align: left; display: flex; align-items: center; gap: 10px; padding: 8px 8px 8px 0; min-height: 56px; }
177
+ .rtxt { display: flex; flex-direction: column; min-width: 0; flex: 1; }
178
+ .rtitle { font-weight: 500; line-height: 1.3; overflow-wrap: anywhere; }
179
+ .rsub { color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
180
+ .flag { flex: none; font-size: 11px; font-weight: 600; border: 1px solid var(--lv, var(--attention-careful)); color: var(--lv, var(--attention-careful)); border-radius: 4px; padding: 0 5px; }
181
+ .emptyrows { padding: 8px 24px 16px; color: var(--muted); }
182
+ .overviewrow { padding: 12px 8px 4px; }
183
+ .overviewrow .row .pick { min-height: 44px; padding-left: 16px; }
184
+ .railfoot { margin-top: auto; padding: 16px; border-top: 1px solid var(--line); font-size: 13px; }
185
+ .railfoot .eyebrow { margin-bottom: 6px; }
186
+ .railfoot a { display: inline-block; padding: 8px 0; }
141
187
 
142
- /* Group cards */
143
- .group {
144
- background: var(--surface); border: 1px solid var(--border); border-left: 4px solid var(--c);
145
- border-radius: 8px; margin: 0 0 12px; scroll-margin-top: 16px;
146
- }
147
- .group:focus-visible, .group.focused { outline: 2px solid var(--accent); outline-offset: 2px; }
148
- .group.hidden { display: none; }
149
- .group-head { padding: 14px 16px 4px; }
150
- .group-title { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; }
151
- .badge {
152
- font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;
153
- color: var(--c); border: 1px solid var(--c); border-radius: 4px; padding: 0 5px;
154
- }
155
- .intent { font-weight: 600; font-size: 16px; overflow-wrap: anywhere; }
156
- .gid { font-family: var(--mono); font-size: 12.5px; color: var(--muted); }
157
- .stats { margin-left: auto; font-family: var(--mono); font-size: 12.5px; color: var(--muted); white-space: nowrap; }
188
+ /* Rail: commits of a range */
189
+ .sum { margin: 16px; padding: 14px 16px; border: 1px solid var(--line); border-radius: 10px; background: var(--bg); }
190
+ .sum .line { margin-top: 8px; color: var(--muted); font-size: 13px; }
191
+ .sum .line b { color: var(--ink); font-weight: 600; }
192
+ .strip { display: flex; gap: 3px; height: 14px; margin-top: 10px; }
193
+ .strip i { display: block; flex: 1; border-radius: 3px; background: var(--lv); min-width: 2px; }
194
+ .strip i.none { background: transparent; border: 1.5px dashed var(--attention-none); }
195
+ .commits { display: flex; flex-direction: column; padding: 0 8px 16px; }
196
+ .crow { margin-bottom: 2px; }
197
+ .crow > .pick { display: block; width: 100%; border: 1px solid transparent; border-radius: 8px; padding: 10px 12px; min-height: 64px; }
198
+ .crow.sel > .pick { background: var(--accent-soft); border-color: var(--accent); }
199
+ .rtop { display: flex; align-items: center; gap: 10px; }
200
+ .rtop .sha { border: 0; background: transparent; padding: 0; color: var(--muted); }
201
+ .lv { margin-left: auto; display: inline-flex; align-items: center; gap: 8px; font-size: 11.5px; font-weight: 600; letter-spacing: 0.05em; color: var(--lv); }
202
+ .csubject { display: block; font-weight: 500; line-height: 1.3; margin-top: 2px; overflow-wrap: anywhere; }
203
+ .cmeta { display: block; color: var(--muted); font-size: 12px; margin-top: 2px; }
204
+ .crow .rows { padding: 2px 0 8px 12px; }
205
+ .mergelist { padding: 0 24px 16px; color: var(--muted); font-size: 13px; }
206
+ .mergelist .eyebrow { margin-bottom: 4px; }
207
+
208
+ /* Detail pane */
209
+ .crumbs { color: var(--muted); font-size: 13px; }
210
+ .commit { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; color: var(--muted); font-size: 13px; margin-bottom: 8px; }
211
+ h1 { font-size: 26px; line-height: 1.25; margin: 6px 0 14px; letter-spacing: -0.015em; font-weight: 600; max-width: 40ch; overflow-wrap: anywhere; }
212
+ h2 { font-size: 15px; margin: 28px 0 10px; }
213
+ .tags { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; }
214
+ .imp { display: inline-flex; align-items: center; gap: 8px; font-weight: 500; color: var(--m); }
215
+ .imp.derived { font-style: italic; }
216
+ .reason { margin: 10px 0 0; color: var(--muted); max-width: 70ch; overflow-wrap: anywhere; }
217
+ .stats { font-family: var(--mono); font-size: 12px; color: var(--muted); white-space: nowrap; }
158
218
  .plus { color: var(--add-mark); }
159
219
  .minus { color: var(--del-mark); }
160
220
  .stale { font-size: 12px; color: var(--warn-text); background: var(--warn-bg); border-radius: 4px; padding: 0 6px; }
161
-
162
- .facts { margin: 8px 0 0; padding: 0 16px 12px; display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px; font-size: 14px; }
163
- .facts dt { color: var(--muted); font-size: 12.5px; padding-top: 1px; }
164
- .facts dd { margin: 0; max-width: 80ch; overflow-wrap: anywhere; }
165
- @media (max-width: 600px) {
166
- .facts { grid-template-columns: 1fr; }
167
- .facts dd { margin-bottom: 6px; }
168
- .stats { margin-left: 0; }
169
- }
170
-
171
- .code-toggle {
172
- display: block; width: 100%; text-align: left; border: 0; border-top: 1px solid var(--border);
173
- background: transparent; color: var(--muted); font: inherit; font-size: 13px; padding: 8px 16px; cursor: pointer;
174
- border-radius: 0 0 8px 0;
175
- }
176
- .code-toggle:hover { background: var(--surface-2); color: var(--text); }
177
- .code { border-top: 1px solid var(--border); padding: 12px 16px 16px; }
178
- .code[hidden] { display: none; }
221
+ .warning { margin: 16px 0 0; padding: 12px 14px; border-radius: 8px; max-width: 720px; background: var(--warn-bg); border: 1px solid var(--warn-border); color: var(--warn-text); }
222
+ .statement { margin: 12px 0 0; max-width: 75ch; overflow-wrap: anywhere; }
223
+ .statement .label { color: var(--muted); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; margin-right: 8px; }
224
+ .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; margin: 24px 0 8px; align-items: start; }
225
+ .card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; min-width: 0; }
226
+ .card p { margin: 4px 0 0; overflow-wrap: anywhere; }
227
+ .files { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
228
+ .frow { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; padding: 8px 16px; border-top: 1px solid var(--line); }
229
+ .frow:first-child { border-top: 0; }
230
+ .frow .mono { overflow-wrap: anywhere; min-width: 0; }
231
+ .chead { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; margin: 28px 0 10px; }
232
+ .chead h2 { margin: 0; }
233
+ .btn { display: inline-flex; align-items: center; border: 1px solid var(--line); background: var(--panel); border-radius: 8px; padding: 8px 16px; min-height: 44px; color: var(--ink); }
234
+ .btn:hover:not(:disabled) { background: var(--accent-soft); }
235
+ .btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 500; }
236
+ .btn.primary:hover:not(:disabled) { background: var(--accent-strong); border-color: var(--accent-strong); }
237
+ .btn.small { min-height: 36px; padding: 4px 12px; font-size: 13px; }
238
+ .nav { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 28px; }
239
+ .keys { margin-left: auto; color: var(--muted); font-size: 12px; align-self: center; }
179
240
 
180
241
  /* Hunks */
181
- .hunk { border: 1px solid var(--border); border-radius: 6px; overflow: hidden; margin: 0 0 12px; }
182
- .hunk:last-child { margin-bottom: 0; }
183
- .hunk-head {
184
- display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px;
185
- padding: 6px 10px; background: var(--surface-2); border-bottom: 1px solid var(--border); font-size: 12.5px;
186
- }
187
- .hunk-head .file { font-family: var(--mono); font-weight: 600; overflow-wrap: anywhere; }
242
+ .hunk { margin-top: 12px; border: 1px solid var(--line); border-radius: 10px; overflow: hidden; background: var(--panel); }
243
+ .hunk-head { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; padding: 8px 14px; background: var(--bg); border-bottom: 1px solid var(--line); font-size: 12.5px; }
244
+ .hunk.closed .hunk-head { border-bottom: 0; }
245
+ .htoggle { display: inline-flex; align-items: center; gap: 8px; border: 0; background: transparent; padding: 6px 4px; min-height: 36px; text-align: left; border-radius: 6px; }
246
+ .htoggle:hover { background: var(--accent-soft); }
247
+ .hunk-head .file { font-family: var(--mono); font-weight: 500; overflow-wrap: anywhere; }
248
+ .chev { color: var(--muted); width: 1em; }
188
249
  .hunk-head .range { font-family: var(--mono); color: var(--muted); }
189
250
  .also { color: var(--muted); }
190
251
  .also a { font-family: var(--mono); }
191
- .scroll { overflow-x: auto; }
192
- table.diff { border-collapse: collapse; width: 100%; font-family: var(--mono); font-size: 12.5px; line-height: 1.5; }
252
+ .scroll { overflow-x: auto; overscroll-behavior-x: contain; }
253
+ table.diff { border-collapse: collapse; width: 100%; font-family: var(--mono); font-size: 12.5px; line-height: 1.6; }
193
254
  table.diff td { padding: 0 8px; vertical-align: top; }
194
- td.ln { width: 1%; min-width: 3.2em; text-align: right; color: var(--muted); user-select: none; opacity: 0.8; }
255
+ td.ln { width: 1%; min-width: 3.2em; text-align: right; color: var(--muted); user-select: none; }
195
256
  td.mark { width: 1%; padding: 0 2px 0 4px; user-select: none; }
196
257
  td.text { white-space: pre; }
197
258
  tr.add { background: var(--add-bg); }
@@ -199,31 +260,90 @@ tr.add td.mark { color: var(--add-mark); }
199
260
  tr.del { background: var(--del-bg); }
200
261
  tr.del td.mark { color: var(--del-mark); }
201
262
  tr.meta td { background: var(--meta-bg); color: var(--meta-text); }
202
- tr.gap td { background: var(--surface-2); color: var(--muted); font-style: italic; font-family: var(--sans); padding: 2px 8px; }
203
-
204
- /* Unannotated, alternatives, risks */
205
- .section { margin-top: 40px; }
206
- .section h2 { font-size: 17px; margin: 0 0 12px; }
207
- .unannotated { border: 1px dashed var(--del-mark); border-radius: 8px; padding: 12px 16px; background: var(--surface); }
208
- .list { margin: 0; padding-left: 20px; max-width: 80ch; }
209
- .list li { margin-bottom: 6px; }
263
+ tr.gap td { background: var(--bg); color: var(--muted); font-style: italic; font-family: var(--sans); padding: 2px 8px; }
264
+
265
+ /* A commit's groups in the detail pane (range pages) */
266
+ .filters { margin: 12px 0 0; }
267
+ .main .necs { padding: 10px 0; }
268
+ .main .rows { padding: 8px; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; }
269
+ .back { margin-bottom: 12px; max-width: 100%; text-align: left; }
270
+
271
+ /* Overview extras */
272
+ .bar { display: flex; height: 10px; border-radius: 5px; overflow: hidden; background: var(--line); max-width: 720px; }
273
+ .bar span { display: block; height: 100%; background: var(--c); min-width: 2px; }
274
+ .bar span + span { border-left: 2px solid var(--panel); }
275
+ .legend { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 8px; font-size: 12.5px; color: var(--muted); }
276
+ .legend .dot { margin-right: 6px; }
277
+ .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; flex: none; background: var(--c, var(--muted)); }
278
+ .legend + .chips { margin-top: 12px; }
279
+ .chips { display: flex; flex-wrap: wrap; gap: 6px; }
280
+ .chips .chip b { font-family: var(--mono); font-weight: 500; font-size: 12px; }
281
+ .chips .chip span { color: var(--muted); }
282
+ .warnbox { margin: 20px 0 0; padding: 16px 18px; border: 1px dashed var(--warn); border-radius: 12px; background: var(--panel); max-width: 720px; }
283
+ .warnbox .t { font-weight: 600; }
284
+ .warnbox p { margin: 6px 0 0; }
285
+ .cmd { font-family: var(--mono); font-size: 12px; color: var(--muted); }
286
+ .unannotated { border: 1px dashed var(--del-mark); border-radius: 10px; padding: 12px 16px; background: var(--panel); }
287
+ .unannotated > p { margin: 0 0 4px; }
288
+ .list { margin: 0; padding-left: 20px; max-width: 70ch; }
289
+ .list li { margin-bottom: 6px; overflow-wrap: anywhere; }
210
290
  .list .why { color: var(--muted); }
211
291
 
212
- footer { margin-top: 48px; color: var(--muted); font-size: 12.5px; }
213
- kbd { font-family: var(--mono); font-size: 11.5px; border: 1px solid var(--border); border-bottom-width: 2px; border-radius: 4px; padding: 0 4px; background: var(--surface); }
214
-
215
- /* Range pages */
216
- .commit-list { list-style: none; margin: 0 0 8px; padding: 0; font-size: 14px; }
217
- .commit-list li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; padding: 3px 0; }
218
- .commit-list .sha { font-size: 12px; }
219
- .commit-section { margin-top: 48px; padding-top: 24px; border-top: 1px solid var(--border); scroll-margin-top: 16px; }
220
- .commit-section h2 { font-size: 20px; margin: 8px 0 12px; }
221
- .commit-section .section h3 { font-size: 16px; margin: 0 0 12px; }
222
- .toc h2.commit-head { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-transform: none; letter-spacing: 0; font-family: var(--mono); }
223
-
224
- /* Reviews recorded for a commit */
225
- .reviews { margin: 8px 0 12px; display: flex; flex-direction: column; gap: 4px; }
226
- .review summary { cursor: pointer; }
292
+ /* Recorded reviews: a compact status block in the rail, the full view in the pane */
293
+ .reviews { display: flex; flex-direction: column; gap: 2px; }
294
+ .rvrow { display: flex; align-items: center; gap: 8px; width: 100%; min-height: 44px; padding: 4px 8px; border: 0; border-radius: 8px; background: transparent; text-align: left; }
295
+ .rvrow:hover { background: var(--accent-soft); }
296
+ .rvdot.ok { color: var(--ok); }
297
+ .rvdot.warn { color: var(--warn); }
298
+ .rvdot.none { color: var(--muted); }
299
+ .rvdot { width: 8px; height: 8px; border-radius: 50%; flex: none; background: currentColor; }
300
+ .rvstage { font-weight: 600; }
301
+ .rvtxt { color: var(--muted); font-size: 12.5px; flex: 1; min-width: 0; overflow-wrap: anywhere; }
302
+ .rvn { color: var(--muted); font-variant-numeric: tabular-nums; flex: none; }
227
303
  .review-ok { color: var(--ok); font-weight: 600; }
228
- .review-none { color: var(--muted); font-size: 0.9em; }
229
- .findings { white-space: pre-wrap; font-size: 0.85em; max-height: 32em; overflow: auto; padding: 8px; border-left: 3px solid var(--border); }
304
+ .review-none { color: var(--muted); }
305
+ .rvstagepane { margin-top: 28px; }
306
+ .rvstagepane h2 { margin: 0 0 6px; font-size: 18px; }
307
+ .rvsum { margin: 0 0 12px; }
308
+ .rvsum .stale { background: transparent; padding: 0; font-size: inherit; color: var(--warn); font-weight: 600; }
309
+ .entry { margin: 0 0 20px; max-width: 75ch; }
310
+ .entry.old { border-top: 1px solid var(--line); padding-top: 8px; }
311
+ .entry summary { cursor: pointer; padding: 6px 0; min-height: 36px; }
312
+ .entry summary .ehead { display: inline-flex; }
313
+ .ehead { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; margin-bottom: 8px; }
314
+ .chip.good { color: var(--ok); border-color: var(--ok); }
315
+ .chip.bad { color: var(--warn); border-color: var(--warn); }
316
+
317
+ /* Rendered markdown */
318
+ .md { max-width: 75ch; overflow-wrap: anywhere; }
319
+ .md p { margin: 0 0 10px; }
320
+ .md h3, .md h4, .md h5, .md h6 { margin: 18px 0 6px; line-height: 1.3; }
321
+ .md h3 { font-size: 17px; }
322
+ .md h4 { font-size: 15px; }
323
+ .md h5, .md h6 { font-size: 14px; }
324
+ .md ul, .md ol { margin: 0 0 10px; padding-left: 22px; }
325
+ .md li { margin-bottom: 4px; }
326
+ .md code { background: var(--bg); border: 1px solid var(--line); border-radius: 4px; padding: 0 4px; }
327
+ .md pre { margin: 0 0 12px; padding: 10px 12px; background: var(--bg); border: 1px solid var(--line); border-radius: 8px; overflow-x: auto; overscroll-behavior-x: contain; }
328
+ .md pre code { border: 0; padding: 0; background: transparent; white-space: pre; overflow-wrap: normal; }
329
+
330
+ footer { padding: 16px; color: var(--muted); font-size: 12.5px; border-top: 1px solid var(--line); }
331
+ footer p { margin: 6px 0 0; }
332
+
333
+ @media (max-width: 600px) {
334
+ h1 { font-size: 22px; }
335
+ .keys { display: none; }
336
+ .track { width: 80px; }
337
+ }
338
+ main footer { padding-left: 0; padding-right: 0; margin-top: 32px; }
339
+ .linkbtn { border: 0; background: transparent; padding: 8px 0; min-height: 36px; color: var(--accent); text-decoration: underline; text-align: left; }
340
+
341
+ .railsec { margin-top: 12px; padding: 12px 16px 4px; border-top: 1px solid var(--line); font-size: 13px; }
342
+ .railsec .eyebrow { margin-bottom: 4px; }
343
+ .overviewrow .lv { margin-left: 8px; flex: none; }
344
+ .focuscards { display: flex; flex-direction: column; gap: 8px; max-width: 720px; }
345
+ .focuscard { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; width: 100%; text-align: left; padding: 12px 16px; min-height: 44px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); }
346
+ .focuscard:hover { background: var(--accent-soft); border-color: var(--accent); }
347
+ .fctitle { font-weight: 600; overflow-wrap: anywhere; }
348
+ .fcmeta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; }
349
+ .fcreason { color: var(--muted); overflow-wrap: anywhere; }
package/dist/loader.js CHANGED
@@ -58,5 +58,5 @@ export function loadGloss(sha) {
58
58
  warnings.push(`this gloss no longer matches the commit (${bad} glossed hunk(s) changed or gone, ${unknown} new); ` +
59
59
  'the commit was probably amended or rebased after glossing. Re-gloss it with `git gloss add --force`.');
60
60
  }
61
- return { sha, subject: commitSubject(sha), note, groups, unannotated, stale: warnings.length > 0, warnings };
61
+ return { sha, subject: commitSubject(sha), note, hunks: current, groups, unannotated, stale: warnings.length > 0, warnings };
62
62
  }
package/dist/merge.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { matchHunks } from './loader.js';
2
+ import { combineAttention } from './attention.js';
2
3
  import { servesOf } from './schema.js';
3
4
  import { bodyLines, changedCount, formatRanges, selections, uncovered } from './selection.js';
4
5
  import { alsoServed } from './validate.js';
@@ -177,6 +178,9 @@ export function mergeGlosses(sources, target) {
177
178
  const summaries = unique(notes.flatMap((a) => (a.summary ? [a.summary] : [])));
178
179
  const alternatives = unique(notes.flatMap((a) => a.alternatives ?? []));
179
180
  const risks = unique(notes.flatMap((a) => a.risks ?? []));
181
+ const rated = used.filter((i) => sources[i].note.annotation.attention);
182
+ // One unrated source makes the whole gloss unrated: its changes have no level to take.
183
+ const attention = rated.length < used.length ? undefined : combineAttention(rated.map((i) => sources[i].note.annotation.attention), (part, id) => newId.get(`${rated[part]}\0${id}`), groups);
180
184
  const annotation = {
181
185
  version: 1,
182
186
  task: tasks.join('; '),
@@ -184,6 +188,7 @@ export function mergeGlosses(sources, target) {
184
188
  groups,
185
189
  ...(alternatives.length ? { alternatives } : {}),
186
190
  ...(risks.length ? { risks } : {}),
191
+ ...(attention ? { attention } : {}),
187
192
  ...(notes[0].author ? { author: notes[0].author } : {}),
188
193
  };
189
194
  const claims = kept.flatMap(({ byHunk }) => [...byHunk].map(([hunk, lines]) => ({ hunk, lines })));