adr-kit 0.9.0 → 0.11.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.
Files changed (76) hide show
  1. package/README.md +23 -7
  2. package/README.zh.md +18 -6
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +18 -5
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/completion.d.ts.map +1 -1
  7. package/dist/commands/completion.js +4 -3
  8. package/dist/commands/completion.js.map +1 -1
  9. package/dist/commands/graph.d.ts +3 -0
  10. package/dist/commands/graph.d.ts.map +1 -1
  11. package/dist/commands/graph.js +39 -6
  12. package/dist/commands/graph.js.map +1 -1
  13. package/dist/commands/supersede.js +1 -1
  14. package/dist/commands/supersede.js.map +1 -1
  15. package/dist/commands/tree.d.ts +1 -1
  16. package/dist/commands/tree.d.ts.map +1 -1
  17. package/dist/commands/tree.js +4 -1
  18. package/dist/commands/tree.js.map +1 -1
  19. package/dist/core/adr.d.ts +1 -0
  20. package/dist/core/adr.d.ts.map +1 -1
  21. package/dist/core/adr.js.map +1 -1
  22. package/dist/core/canvas-view.d.ts +21 -0
  23. package/dist/core/canvas-view.d.ts.map +1 -0
  24. package/dist/core/canvas-view.js +316 -0
  25. package/dist/core/canvas-view.js.map +1 -0
  26. package/dist/core/deliberation-html.d.ts +4 -0
  27. package/dist/core/deliberation-html.d.ts.map +1 -0
  28. package/dist/core/deliberation-html.js +153 -0
  29. package/dist/core/deliberation-html.js.map +1 -0
  30. package/dist/core/deliberation-view.d.ts +4 -0
  31. package/dist/core/deliberation-view.d.ts.map +1 -0
  32. package/dist/core/deliberation-view.js +341 -0
  33. package/dist/core/deliberation-view.js.map +1 -0
  34. package/dist/core/deliberation.d.ts +39 -7
  35. package/dist/core/deliberation.d.ts.map +1 -1
  36. package/dist/core/deliberation.js +175 -35
  37. package/dist/core/deliberation.js.map +1 -1
  38. package/dist/core/graph-html.d.ts +8 -0
  39. package/dist/core/graph-html.d.ts.map +1 -0
  40. package/dist/core/graph-html.js +299 -0
  41. package/dist/core/graph-html.js.map +1 -0
  42. package/dist/core/graph-view.d.ts +3 -0
  43. package/dist/core/graph-view.d.ts.map +1 -0
  44. package/dist/core/graph-view.js +80 -0
  45. package/dist/core/graph-view.js.map +1 -0
  46. package/dist/core/graph.d.ts +4 -1
  47. package/dist/core/graph.d.ts.map +1 -1
  48. package/dist/core/graph.js +11 -2
  49. package/dist/core/graph.js.map +1 -1
  50. package/dist/core/relation-focus.d.ts +4 -0
  51. package/dist/core/relation-focus.d.ts.map +1 -0
  52. package/dist/core/relation-focus.js +89 -0
  53. package/dist/core/relation-focus.js.map +1 -0
  54. package/dist/core/repository.d.ts.map +1 -1
  55. package/dist/core/repository.js +6 -2
  56. package/dist/core/repository.js.map +1 -1
  57. package/dist/core/share.d.ts +30 -0
  58. package/dist/core/share.d.ts.map +1 -0
  59. package/dist/core/share.js +420 -0
  60. package/dist/core/share.js.map +1 -0
  61. package/dist/core/templates.d.ts +10 -3
  62. package/dist/core/templates.d.ts.map +1 -1
  63. package/dist/core/templates.js +12 -2
  64. package/dist/core/templates.js.map +1 -1
  65. package/dist/core/tool-integrations.d.ts.map +1 -1
  66. package/dist/core/tool-integrations.js +55 -5
  67. package/dist/core/tool-integrations.js.map +1 -1
  68. package/dist/core/validate.d.ts +1 -1
  69. package/dist/core/validate.d.ts.map +1 -1
  70. package/dist/core/validate.js +56 -15
  71. package/dist/core/validate.js.map +1 -1
  72. package/dist/core/view-text.d.ts +21 -0
  73. package/dist/core/view-text.d.ts.map +1 -0
  74. package/dist/core/view-text.js +92 -0
  75. package/dist/core/view-text.js.map +1 -0
  76. package/package.json +1 -1
@@ -0,0 +1,341 @@
1
+ /** Inline assets for the offline tree viewer. No network or runtime dependency. */
2
+ export const TREE_STYLE = String.raw `
3
+
4
+ :root {
5
+ font-family: Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
6
+ color: #233c36;
7
+ background: #f6f8f5;
8
+ font-synthesis: none
9
+ }
10
+ * {
11
+ box-sizing: border-box
12
+ }
13
+ body {
14
+ margin: 0
15
+ }
16
+ button {
17
+ font: inherit;
18
+ color: inherit
19
+ }
20
+ button {
21
+ cursor: pointer
22
+ }
23
+ button:focus-visible,summary:focus-visible,main:focus-visible {
24
+ outline: 3px solid #9775ce;
25
+ outline-offset: 4px
26
+ }
27
+ button {
28
+ border: 1px solid #dce4dd;
29
+ background: white;
30
+ border-radius: 8px;
31
+ padding: 9px 13px;
32
+ font-size: 12px
33
+ }
34
+ button:hover {
35
+ background: #edf3ef
36
+ }
37
+ main {
38
+ position: relative;
39
+ background-image: radial-gradient(#cdd8cc .8px,transparent .8px);
40
+ background-size: 20px 20px;
41
+ }
42
+ #world {
43
+ position: absolute;
44
+ transform-origin: 0 0
45
+ }
46
+ #edges {
47
+ position: absolute;
48
+ inset: 0;
49
+ overflow: visible;
50
+ pointer-events: none
51
+ }
52
+ .card {
53
+ width: 292px;
54
+ background: #fff;
55
+ border: 1px solid #dce4dc;
56
+ border-radius: 13px;
57
+ box-shadow: 0 3px 8px #25403506;
58
+ overflow: visible
59
+ }
60
+ .card:hover {
61
+ border-color: #a6bcae
62
+ }
63
+ .cardhead {
64
+ padding: 19px 20px 15px
65
+ }
66
+ .label {
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: space-between;
70
+ gap: 8px;
71
+ font-size: 10px;
72
+ color: #78877e;
73
+ letter-spacing: 1px;
74
+ margin-bottom: 12px
75
+ }
76
+ .state {
77
+ color: #387456;
78
+ letter-spacing: 0;
79
+ background: #edf6ef;
80
+ border-radius: 4px;
81
+ padding: 3px 6px
82
+ }
83
+ .card h2 {
84
+ font-size: 16px;
85
+ line-height: 1.45;
86
+ letter-spacing: -.25px;
87
+ margin: 0;
88
+ font-weight: 600
89
+ }
90
+ .answer {
91
+ margin: 0 13px 13px;
92
+ padding: 13px;
93
+ background: #f1f7f1;
94
+ color: #264c38;
95
+ border: 1px solid #e2ece1;
96
+ border-radius: 8px;
97
+ position: relative
98
+ }
99
+ .answer p {
100
+ font-size: 13px;
101
+ line-height: 1.5;
102
+ margin: 8px 0 0
103
+ }
104
+ .badge {
105
+ font-size: 10px;
106
+ color: #407455
107
+ }
108
+ .answer .reason {
109
+ font-size: 11px;
110
+ color: #748576;
111
+ line-height: 1.55
112
+ }
113
+ .override {
114
+ background: #f4effb;
115
+ color: #7750a5;
116
+ font-size: 10px;
117
+ padding: 4px 6px;
118
+ border-radius: 4px;
119
+ letter-spacing: 0
120
+ }
121
+ .alts {
122
+ border-top: 1px solid #e7ece6;
123
+ padding: 12px 20px;
124
+ color: #748078;
125
+ font-size: 11px
126
+ }
127
+ .alts summary {
128
+ cursor: pointer;
129
+ list-style: none;
130
+ display: flex;
131
+ justify-content: space-between
132
+ }
133
+ .alts summary:after {
134
+ content: '+';
135
+ font-size: 15px
136
+ }
137
+ .alts[open] summary:after {
138
+ content: '−'
139
+ }
140
+ .alt {
141
+ padding-top: 15px;
142
+ margin-top: 12px;
143
+ border-top: 1px solid #eef0ed;
144
+ line-height: 1.55;
145
+ font-size: 12px;
146
+ color: #59645d
147
+ }
148
+ .alt p {
149
+ margin: 5px 0;
150
+ font-size: 11px;
151
+ color: #788279
152
+ }
153
+ .root {
154
+ background: #294d3e;
155
+ border-color: #294d3e;
156
+ color: #fff;
157
+ width: 245px
158
+ }
159
+ .root .cardhead {
160
+ padding: 25px
161
+ }
162
+ .root .label {
163
+ color: #bacdbc
164
+ }
165
+ .root h2 {
166
+ font-size: 21px;
167
+ font-weight: 500;
168
+ line-height: 1.4
169
+ }
170
+ .virtual {
171
+ width: 168px;
172
+ background: #fbfdfb;
173
+ border-style: dashed;
174
+ border-color: #b6c7ba;
175
+ }
176
+ .virtual .cardhead {
177
+ padding: 14px 16px 12px;
178
+ }
179
+ .branch {
180
+ position: absolute;
181
+ right: -13px;
182
+ bottom: 25px;
183
+ padding: 0;
184
+ width: 26px;
185
+ height: 26px;
186
+ border-radius: 50%;
187
+ background: white;
188
+ color: #5a7d64;
189
+ font-size: 15px;
190
+ box-shadow: 0 2px 5px #274b3910;
191
+ z-index: 2
192
+ }
193
+ .card { overflow-wrap: anywhere; }
194
+ .cardhead .reason { font-size: 12px; color: #67786c; line-height: 1.6; }
195
+ .root .reason { color: #bed0c2; }
196
+ .state.open { color: #805f0e; background: #fff5d6; }
197
+ .state.rejected { color: #96554c; background: #faeeeb; }
198
+ .state.unrecorded { color: #647369; background: #eff2ee; }
199
+ .root .state { color: #264c38; }
200
+ .root .alts { color: #bacdbc; border-top-color: #3f6455; }
201
+ .root .alt { color: #cfdcd1; border-top-color: #3f6455; }
202
+ .root .alt .badge { color: #a3bba9; }
203
+ .root .alt p { color: #b4c4b6; }
204
+ .override-key { color: #7750a5; }
205
+ /* The legend's state keys quote the card chips, so they match their size. */
206
+ .canvas-legend .legend .state { font-size: 10px; }
207
+ /* The edge keys are line samples, drawn in the stroke the view actually uses:
208
+ a key that looks like a line but not like the line it names teaches nothing. */
209
+ .edge-key { width: 22px; height: 0; border-top: 1.5px solid #b6c7ba; }
210
+ .edge-key.unlocked { border-top: 2px solid #7ca38a; }
211
+ .option { border-style: dashed; }
212
+ .card[hidden] { display: none; }
213
+ .card { position: relative; margin: 18px 24px 18px calc(24px + var(--depth) * 24px); }
214
+ #world { position: relative; }
215
+ main { height: auto; min-height: 0; overflow: auto; touch-action: auto; cursor: auto; }
216
+ #edges, .branch, .js-control { display: none; }
217
+
218
+ .interactive #world { position: absolute; }
219
+ .interactive .card { position: absolute; margin: 0; }
220
+ .interactive #edges, .interactive .branch, .interactive .js-control { display: block; }
221
+ .no-script { padding: 20px; }
222
+ `;
223
+ // Kept as readable JavaScript: tsc ships this string unchanged in the CLI bundle.
224
+ // All record text is server-escaped HTML, never interpolated into this program.
225
+ export const TREE_SCRIPT = String.raw `
226
+ (() => {
227
+ const canvas = window.__adrCanvas;
228
+ if (!canvas) return;
229
+ const { viewport, world } = canvas;
230
+ const svg = document.getElementById('edges');
231
+ const cards = Array.from(document.querySelectorAll('.card'));
232
+ if (!cards.length) return;
233
+ const items = cards.map(card => ({
234
+ card, id: card.id, parentId: card.dataset.parent,
235
+ anchor: document.getElementById(card.dataset.anchor),
236
+ children: [], x: 0, y: 0, width: 0, height: 0, span: 0
237
+ }));
238
+ const byId = new Map(items.map(item => [item.id, item]));
239
+ const roots = items.filter(item => !item.parentId);
240
+ for (const item of items) {
241
+ if (item.parentId) byId.get(item.parentId).children.push(item);
242
+ }
243
+ const collapsed = new Set();
244
+
245
+ function updateVisibility(item, hidden) {
246
+ item.card.hidden = hidden;
247
+ for (const child of item.children) {
248
+ const disclosure = child.anchor && child.anchor.closest('details');
249
+ updateVisibility(child, hidden || collapsed.has(item.id) || !!(disclosure && !disclosure.open));
250
+ }
251
+ }
252
+
253
+ function measure(item) {
254
+ item.width = item.card.offsetWidth;
255
+ item.height = item.card.offsetHeight;
256
+ const children = item.children.filter(child => !child.card.hidden);
257
+ const childrenHeight = children.reduce((sum, child) => sum + measure(child), 0);
258
+ item.span = Math.max(item.height, childrenHeight + Math.max(0, children.length - 1) * 34);
259
+ return item.span;
260
+ }
261
+
262
+ function place(item, x, y) {
263
+ item.x = x;
264
+ item.y = y + (item.span - item.height) / 2;
265
+ item.card.style.left = item.x + 'px';
266
+ item.card.style.top = item.y + 'px';
267
+ let nextY = y;
268
+ for (const child of item.children) {
269
+ if (child.card.hidden) continue;
270
+ place(child, x + item.width + 94, nextY);
271
+ nextY += child.span + 34;
272
+ }
273
+ }
274
+
275
+ function layout() {
276
+ roots.forEach(root => updateVisibility(root, false));
277
+ roots.forEach(measure);
278
+ let nextY = 28;
279
+ for (const root of roots) {
280
+ place(root, 28, nextY);
281
+ nextY += root.span + 36;
282
+ }
283
+ const visible = items.filter(item => !item.card.hidden);
284
+ const width = visible.reduce((max, item) => Math.max(max, item.x + item.width), 0) + 28;
285
+ const height = visible.reduce((max, item) => Math.max(max, item.y + item.height), 0) + 28;
286
+ world.style.width = width + 'px';
287
+ world.style.height = height + 'px';
288
+ svg.setAttribute('width', width);
289
+ svg.setAttribute('height', height);
290
+ svg.replaceChildren();
291
+ for (const item of visible) {
292
+ if (!item.parentId) continue;
293
+ const parent = byId.get(item.parentId);
294
+ const source = item.anchor || parent.card.querySelector('.cardhead');
295
+ const sourceRect = source.getBoundingClientRect();
296
+ const parentRect = parent.card.getBoundingClientRect();
297
+ const startY = parent.y + (sourceRect.top - parentRect.top + sourceRect.height / 2) / canvas.scale;
298
+ const startX = parent.x + parent.width;
299
+ const endX = item.x, endY = item.y + Math.min(65, item.height / 2);
300
+ const midpoint = (startX + endX) / 2;
301
+ // The server settled whether this edge is an unlocked one, so the styler
302
+ // and the legend cannot disagree about which edges are frontier steps.
303
+ const unlocked = item.card.dataset.unlocked === 'true';
304
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
305
+ path.setAttribute('d', 'M' + startX + ',' + startY + ' C' + midpoint + ',' + startY
306
+ + ' ' + midpoint + ',' + endY + ' ' + endX + ',' + endY);
307
+ path.setAttribute('fill', 'none');
308
+ path.setAttribute('stroke', unlocked ? '#7ca38a' : '#b6c7ba');
309
+ path.setAttribute('stroke-width', unlocked ? '2' : '1.5');
310
+ path.dataset.from = item.anchor ? item.anchor.id : parent.id;
311
+ path.dataset.to = item.id;
312
+ path.dataset.unlocked = String(unlocked);
313
+ svg.append(path);
314
+ }
315
+ canvas.paint();
316
+ world.dispatchEvent(new Event('adr-layout'));
317
+ }
318
+ window.__adrCanvasLayout = layout;
319
+
320
+ for (const item of items) {
321
+ const button = item.card.querySelector('.branch');
322
+ if (!button) continue;
323
+ button.addEventListener('click', () => {
324
+ if (collapsed.has(item.id)) collapsed.delete(item.id);
325
+ else collapsed.add(item.id);
326
+ button.textContent = collapsed.has(item.id) ? '+' : '−';
327
+ button.setAttribute('aria-expanded', String(!collapsed.has(item.id)));
328
+ const before = item.card.getBoundingClientRect();
329
+ layout();
330
+ const after = item.card.getBoundingClientRect();
331
+ canvas.addPan(before.left - after.left, before.top - after.top);
332
+ });
333
+ }
334
+ for (const detail of document.querySelectorAll('.alts')) detail.addEventListener('toggle', () => canvas.schedule());
335
+
336
+ layout();
337
+ canvas.fitWidth();
338
+ })();
339
+
340
+ `;
341
+ //# sourceMappingURL=deliberation-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deliberation-view.js","sourceRoot":"","sources":["../../src/core/deliberation-view.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4NnC,CAAC;AAEF,kFAAkF;AAClF,gFAAgF;AAChF,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmHpC,CAAC"}
@@ -1,21 +1,53 @@
1
1
  /**
2
- * The optional `## Deliberation` appendix: a grilling session's design tree,
2
+ * The optional ## Deliberation appendix: a grilling session's design tree,
3
3
  * stored as a nested Markdown list so it stays diffable and readable, and
4
- * rendered to mermaid or text on demand. Mermaid is never the source of truth.
4
+ * rendered to text, mermaid, or a single HTML document on demand. The stored
5
+ * outline is the source of truth; every renderer is a view.
6
+ *
7
+ * Grammar (ADR 5):
8
+ *
9
+ * - [Q: | A: ]<text>[ [status]][ (recommended)][ em-dash <reason>]
10
+ *
11
+ * Q:/A: marks a question or an option (inferred when omitted: a node with
12
+ * children is a question, a leaf is an option, the top level is the root).
13
+ * [settled], [rejected], [open] is the state. (recommended) marks the option
14
+ * the agent recommended. The em-dash reason explains the node.
15
+ *
16
+ * Dependency is nesting (ADR 7): a follow-up question is a child of the node
17
+ * whose settlement raised it, so related questions run deeper and unrelated
18
+ * ones stay flat. Nothing stores a round; depth is the frontier.
5
19
  */
6
20
  export type DeliberationStatus = 'settled' | 'rejected' | 'open';
21
+ export type DeliberationType = 'question' | 'option';
7
22
  export interface DeliberationNode {
8
23
  text: string;
24
+ type?: DeliberationType;
9
25
  status?: DeliberationStatus;
26
+ recommended?: boolean;
27
+ reason?: string;
10
28
  children: DeliberationNode[];
11
29
  }
30
+ export declare function parseDeliberation(body: string | undefined): DeliberationNode[];
12
31
  /**
13
- * Parse the nested list under `## Deliberation`. Indentation defines the
14
- * tree; a trailing `[settled]`, `[rejected]`, or `[open]` marks a node.
15
- * Non-bullet lines are commentary and ignored, so a session can interleave
16
- * prose without breaking the tree.
32
+ * True when the body parses to at least one node: the same predicate `tree`
33
+ * renders by, so a marker can never promise a tree the command refuses.
17
34
  */
18
- export declare function parseDeliberation(body: string | undefined): DeliberationNode[];
35
+ export declare function hasDeliberationTree(body: string | undefined): boolean;
36
+ /**
37
+ * A question whose answer (its settled child) is not the option the agent
38
+ * recommended. Unknown unless both a settled child and a recommended child
39
+ * exist.
40
+ */
41
+ /**
42
+ * Whether the node that raised a follow-up question unlocked it. In the outline
43
+ * a question nests under the node it depends on, so that parent is the node
44
+ * that raised it: a settled parent that is not the tree root turns the edge
45
+ * into frontier progress. The card tree, the drawn edges, and Mermaid all ask
46
+ * this one question rather than each deciding for itself.
47
+ */
48
+ export declare function isUnlockedQuestion(child: DeliberationNode, parent: DeliberationNode, parentIsRoot: boolean): boolean;
19
49
  export declare function renderDeliberationText(nodes: DeliberationNode[]): string;
20
50
  export declare function renderDeliberationMermaid(nodes: DeliberationNode[]): string;
51
+ /** An offline HTML view; the annotated outline remains the source. */
52
+ export declare function renderDeliberationHtml(nodes: DeliberationNode[], title: string): string;
21
53
  //# sourceMappingURL=deliberation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deliberation.d.ts","sourceRoot":"","sources":["../../src/core/deliberation.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,UAAU,GAAG,MAAM,CAAC;AAEjE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,QAAQ,EAAE,gBAAgB,EAAE,CAAC;CAC9B;AAKD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,gBAAgB,EAAE,CAyB9E;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAWxE;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAuB3E"}
1
+ {"version":3,"file":"deliberation.d.ts","sourceRoot":"","sources":["../../src/core/deliberation.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,UAAU,GAAG,MAAM,CAAC;AACjE,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,QAAQ,CAAC;AAErD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,gBAAgB,EAAE,CAAC;CAC9B;AA4DD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,gBAAgB,EAAE,CAsB9E;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAErE;AAQD;;;;GAIG;AACH;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,gBAAgB,EACvB,MAAM,EAAE,gBAAgB,EACxB,YAAY,EAAE,OAAO,GACpB,OAAO,CAET;AAkBD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAYxE;AAkBD,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,gBAAgB,EAAE,GAAG,MAAM,CA8D3E;AAED,sEAAsE;AACtE,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,gBAAgB,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvF"}
@@ -1,16 +1,58 @@
1
- /**
2
- * The optional `## Deliberation` appendix: a grilling session's design tree,
3
- * stored as a nested Markdown list so it stays diffable and readable, and
4
- * rendered to mermaid or text on demand. Mermaid is never the source of truth.
5
- */
1
+ import { renderCardTree } from './deliberation-html.js';
2
+ import { wrapText } from './view-text.js';
6
3
  const BULLET = /^(\s*)-\s+(.*?)\s*$/;
7
- const STATUS = /\s*\[(settled|rejected|open)\]\s*$/i;
4
+ const STATUS = /\[(settled|rejected|open)\]/i;
5
+ const RECOMMENDED = /\(recommended\)\s*$/i;
6
+ const TYPE = /^([QA]):\s+/i;
7
+ const EM_DASH = ' \u2014 ';
8
8
  /**
9
- * Parse the nested list under `## Deliberation`. Indentation defines the
10
- * tree; a trailing `[settled]`, `[rejected]`, or `[open]` marks a node.
11
- * Non-bullet lines are commentary and ignored, so a session can interleave
12
- * prose without breaking the tree.
9
+ * Split a trailing ` — reason` off the node text. The em dash with spaces is
10
+ * the only separator the grammar reserves, which leaves a hyphen free for
11
+ * prose: a ` - ` inside the text can never cut the state or the recommendation
12
+ * into the reason.
13
13
  */
14
+ function splitReason(raw) {
15
+ const emDash = raw.lastIndexOf(EM_DASH);
16
+ if (emDash === -1)
17
+ return { content: raw };
18
+ return { content: raw.slice(0, emDash).trimEnd(), reason: raw.slice(emDash + EM_DASH.length).trim() };
19
+ }
20
+ /** Parse one bullet from the end inward, so a reason never hides the status. */
21
+ function parseNodeText(raw) {
22
+ const split = splitReason(raw);
23
+ let content = split.content;
24
+ const reason = split.reason;
25
+ let recommended;
26
+ if (RECOMMENDED.test(content)) {
27
+ recommended = true;
28
+ content = content.replace(RECOMMENDED, '').trimEnd();
29
+ }
30
+ let status;
31
+ const statusMatch = content.match(STATUS);
32
+ if (statusMatch !== null && statusMatch.index !== undefined) {
33
+ status = statusMatch[1].toLowerCase();
34
+ content = (content.slice(0, statusMatch.index) + content.slice(statusMatch.index + statusMatch[0].length)).trim();
35
+ }
36
+ let type;
37
+ const typeMatch = content.match(TYPE);
38
+ if (typeMatch !== null) {
39
+ type = typeMatch[1].toUpperCase() === 'Q' ? 'question' : 'option';
40
+ content = content.slice(typeMatch[0].length).trim();
41
+ }
42
+ const text = content.trim();
43
+ if (text.length === 0)
44
+ return undefined;
45
+ const node = { text };
46
+ if (type !== undefined)
47
+ node.type = type;
48
+ if (status !== undefined)
49
+ node.status = status;
50
+ if (recommended !== undefined)
51
+ node.recommended = recommended;
52
+ if (reason !== undefined && reason.length > 0)
53
+ node.reason = reason;
54
+ return node;
55
+ }
14
56
  export function parseDeliberation(body) {
15
57
  if (body === undefined)
16
58
  return [];
@@ -21,18 +63,18 @@ export function parseDeliberation(body) {
21
63
  if (match === null)
22
64
  continue;
23
65
  const indent = match[1].replace(/\t/g, ' ').length;
24
- let text = match[2];
25
- let status;
26
- const statusMatch = text.match(STATUS);
27
- if (statusMatch !== null && statusMatch.index !== undefined) {
28
- status = statusMatch[1].toLowerCase();
29
- text = text.slice(0, statusMatch.index).trim();
30
- }
31
- if (text.length === 0)
66
+ const parsed = parseNodeText(match[2]);
67
+ if (parsed === undefined)
32
68
  continue;
33
- const node = { text, children: [] };
34
- if (status !== undefined)
35
- node.status = status;
69
+ const node = { text: parsed.text, children: [] };
70
+ if (parsed.type !== undefined)
71
+ node.type = parsed.type;
72
+ if (parsed.status !== undefined)
73
+ node.status = parsed.status;
74
+ if (parsed.recommended !== undefined)
75
+ node.recommended = parsed.recommended;
76
+ if (parsed.reason !== undefined)
77
+ node.reason = parsed.reason;
36
78
  while (stack.length > 0 && stack[stack.length - 1].indent >= indent)
37
79
  stack.pop();
38
80
  const parent = stack[stack.length - 1];
@@ -44,39 +86,137 @@ export function parseDeliberation(body) {
44
86
  }
45
87
  return roots;
46
88
  }
89
+ /**
90
+ * True when the body parses to at least one node: the same predicate `tree`
91
+ * renders by, so a marker can never promise a tree the command refuses.
92
+ */
93
+ export function hasDeliberationTree(body) {
94
+ return parseDeliberation(body).length > 0;
95
+ }
96
+ /** Explicit Q:/A:, else children decide. */
97
+ function typeOf(node) {
98
+ if (node.type !== undefined)
99
+ return node.type;
100
+ return node.children.length > 0 ? 'question' : 'option';
101
+ }
102
+ /**
103
+ * A question whose answer (its settled child) is not the option the agent
104
+ * recommended. Unknown unless both a settled child and a recommended child
105
+ * exist.
106
+ */
107
+ /**
108
+ * Whether the node that raised a follow-up question unlocked it. In the outline
109
+ * a question nests under the node it depends on, so that parent is the node
110
+ * that raised it: a settled parent that is not the tree root turns the edge
111
+ * into frontier progress. The card tree, the drawn edges, and Mermaid all ask
112
+ * this one question rather than each deciding for itself.
113
+ */
114
+ export function isUnlockedQuestion(child, parent, parentIsRoot) {
115
+ return typeOf(child) === 'question' && !parentIsRoot && parent.status === 'settled';
116
+ }
117
+ function isOverride(node) {
118
+ const settled = node.children.filter((child) => child.status === 'settled');
119
+ const recommended = node.children.filter((child) => child.recommended === true);
120
+ if (settled.length === 0 || recommended.length === 0)
121
+ return false;
122
+ return !settled.some((child) => child.recommended === true);
123
+ }
124
+ function markers(node) {
125
+ const parts = [];
126
+ if (node.status !== undefined)
127
+ parts.push('[' + node.status + ']');
128
+ if (node.recommended === true)
129
+ parts.push('(recommended)');
130
+ if (isOverride(node))
131
+ parts.push('(override)');
132
+ if (node.reason !== undefined)
133
+ parts.push('\u2014 ' + node.reason);
134
+ return parts.join(' ');
135
+ }
47
136
  export function renderDeliberationText(nodes) {
48
137
  const lines = [];
49
138
  const walk = (list, depth) => {
50
139
  for (const node of list) {
51
- const suffix = node.status === undefined ? '' : ` [${node.status}]`;
52
- lines.push(`${' '.repeat(depth)}- ${node.text}${suffix}`);
140
+ const prefix = depth === 0 && node.type === undefined ? '' : (typeOf(node) === 'question' ? 'Q: ' : 'A: ');
141
+ const suffix = markers(node);
142
+ lines.push(' '.repeat(depth) + '- ' + prefix + node.text + (suffix.length > 0 ? ' ' + suffix : ''));
53
143
  walk(node.children, depth + 1);
54
144
  }
55
145
  };
56
146
  walk(nodes, 0);
57
147
  return lines.join('\n');
58
148
  }
149
+ function escapeLabel(text) {
150
+ return text.replace(/"/g, "'");
151
+ }
152
+ /** Wrap a long label for mermaid's HTML labels. */
153
+ function wrapLabel(text, width = 48) {
154
+ return wrapText(text, width).join('<br/>');
155
+ }
59
156
  export function renderDeliberationMermaid(nodes) {
60
- const lines = ['graph TD'];
61
- const byStatus = { settled: [], rejected: [], open: [] };
157
+ const entries = [];
62
158
  let counter = 0;
63
- const walk = (list, parentId) => {
159
+ const collect = (list, parentId, depth) => {
64
160
  for (const node of list) {
65
- const id = `n${++counter}`;
66
- lines.push(` ${id}["${node.text.replace(/"/g, "'")}"]`);
161
+ const id = 'n' + (++counter);
162
+ const entry = { id, node, isRoot: depth === 0 && node.type === undefined };
67
163
  if (parentId !== undefined)
68
- lines.push(` ${parentId} --> ${id}`);
69
- if (node.status !== undefined)
70
- byStatus[node.status].push(id);
71
- walk(node.children, id);
164
+ entry.parentId = parentId;
165
+ entries.push(entry);
166
+ collect(node.children, id, depth + 1);
72
167
  }
73
168
  };
74
- walk(nodes, undefined);
75
- lines.push(' classDef settled fill:#dcfce7,stroke:#16a34a;', ' classDef rejected fill:#fee2e2,stroke:#dc2626;', ' classDef open fill:#fef9c3,stroke:#ca8a04;');
169
+ collect(nodes, undefined, 0);
170
+ const lines = ['graph TD'];
171
+ const labelOf = (node) => wrapLabel(escapeLabel(node.reason === undefined ? node.text : node.text + ' \u2014 ' + node.reason));
172
+ for (const entry of entries) {
173
+ const label = labelOf(entry.node);
174
+ const shape = entry.isRoot
175
+ ? '(["' + label + '"])'
176
+ : (typeOf(entry.node) === 'question' ? '{{"' + label + '"}}' : '["' + label + '"]');
177
+ lines.push(' ' + entry.id + shape);
178
+ }
179
+ // A settled non-root node unlocks its follow-up questions. Unsettled
180
+ // branches retain ordinary edges rather than claiming frontier progress.
181
+ const byId = new Map(entries.map((entry) => [entry.id, entry]));
182
+ const unlockEdges = [];
183
+ let edgeIndex = 0;
184
+ for (const entry of entries) {
185
+ if (entry.parentId === undefined)
186
+ continue;
187
+ const parent = byId.get(entry.parentId);
188
+ lines.push(' ' + entry.parentId + ' --> ' + entry.id);
189
+ if (isUnlockedQuestion(entry.node, parent.node, parent.isRoot)) {
190
+ unlockEdges.push(edgeIndex);
191
+ }
192
+ edgeIndex++;
193
+ }
194
+ const byStatus = { settled: [], rejected: [], open: [] };
195
+ const recommended = [];
196
+ const overridden = [];
197
+ for (const entry of entries) {
198
+ if (entry.node.status !== undefined)
199
+ byStatus[entry.node.status].push(entry.id);
200
+ if (entry.node.recommended === true)
201
+ recommended.push(entry.id);
202
+ if (isOverride(entry.node))
203
+ overridden.push(entry.id);
204
+ }
205
+ lines.push(' classDef settled fill:#dcfce7,stroke:#16a34a;', ' classDef rejected fill:#fee2e2,stroke:#dc2626;', ' classDef open fill:#fef9c3,stroke:#ca8a04;', ' classDef recommended stroke-width:3px;', ' classDef override stroke:#7c3aed,stroke-width:2px,stroke-dasharray:4 2;');
76
206
  for (const status of ['settled', 'rejected', 'open']) {
77
207
  if (byStatus[status].length > 0)
78
- lines.push(` class ${byStatus[status].join(',')} ${status};`);
208
+ lines.push(' class ' + byStatus[status].join(',') + ' ' + status + ';');
79
209
  }
210
+ if (recommended.length > 0)
211
+ lines.push(' class ' + recommended.join(',') + ' recommended;');
212
+ if (overridden.length > 0)
213
+ lines.push(' class ' + overridden.join(',') + ' override;');
214
+ if (unlockEdges.length > 0)
215
+ lines.push(' linkStyle ' + unlockEdges.join(',') + ' stroke:#7c3aed,stroke-width:3px;');
80
216
  return lines.join('\n');
81
217
  }
218
+ /** An offline HTML view; the annotated outline remains the source. */
219
+ export function renderDeliberationHtml(nodes, title) {
220
+ return renderCardTree(nodes, title);
221
+ }
82
222
  //# sourceMappingURL=deliberation.js.map