@issuegraph/editor 0.1.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/LICENSE +201 -0
- package/README.md +238 -0
- package/dist/audit/findings.d.ts +225 -0
- package/dist/audit/findings.d.ts.map +1 -0
- package/dist/audit/findings.js +348 -0
- package/dist/audit/findings.js.map +1 -0
- package/dist/audit/styles.d.ts +37 -0
- package/dist/audit/styles.d.ts.map +1 -0
- package/dist/audit/styles.js +72 -0
- package/dist/audit/styles.js.map +1 -0
- package/dist/audit/surface.d.ts +155 -0
- package/dist/audit/surface.d.ts.map +1 -0
- package/dist/audit/surface.js +202 -0
- package/dist/audit/surface.js.map +1 -0
- package/dist/create/draft.d.ts +119 -0
- package/dist/create/draft.d.ts.map +1 -0
- package/dist/create/draft.js +114 -0
- package/dist/create/draft.js.map +1 -0
- package/dist/create/keys.d.ts +225 -0
- package/dist/create/keys.d.ts.map +1 -0
- package/dist/create/keys.js +228 -0
- package/dist/create/keys.js.map +1 -0
- package/dist/create/placement.d.ts +69 -0
- package/dist/create/placement.d.ts.map +1 -0
- package/dist/create/placement.js +76 -0
- package/dist/create/placement.js.map +1 -0
- package/dist/firstpass/batch.d.ts +190 -0
- package/dist/firstpass/batch.d.ts.map +1 -0
- package/dist/firstpass/batch.js +143 -0
- package/dist/firstpass/batch.js.map +1 -0
- package/dist/firstpass/candidates.d.ts +128 -0
- package/dist/firstpass/candidates.d.ts.map +1 -0
- package/dist/firstpass/candidates.js +39 -0
- package/dist/firstpass/candidates.js.map +1 -0
- package/dist/firstpass/keys.d.ts +90 -0
- package/dist/firstpass/keys.d.ts.map +1 -0
- package/dist/firstpass/keys.js +112 -0
- package/dist/firstpass/keys.js.map +1 -0
- package/dist/firstpass/queue.d.ts +183 -0
- package/dist/firstpass/queue.d.ts.map +1 -0
- package/dist/firstpass/queue.js +159 -0
- package/dist/firstpass/queue.js.map +1 -0
- package/dist/firstpass/render.d.ts +100 -0
- package/dist/firstpass/render.d.ts.map +1 -0
- package/dist/firstpass/render.js +216 -0
- package/dist/firstpass/render.js.map +1 -0
- package/dist/firstpass/styles.d.ts +36 -0
- package/dist/firstpass/styles.d.ts.map +1 -0
- package/dist/firstpass/styles.js +148 -0
- package/dist/firstpass/styles.js.map +1 -0
- package/dist/firstpass/view.d.ts +82 -0
- package/dist/firstpass/view.d.ts.map +1 -0
- package/dist/firstpass/view.js +60 -0
- package/dist/firstpass/view.js.map +1 -0
- package/dist/firstpass/words.d.ts +105 -0
- package/dist/firstpass/words.d.ts.map +1 -0
- package/dist/firstpass/words.js +28 -0
- package/dist/firstpass/words.js.map +1 -0
- package/dist/index.d.ts +150 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +147 -0
- package/dist/index.js.map +1 -0
- package/dist/overlay/grammar.d.ts +259 -0
- package/dist/overlay/grammar.d.ts.map +1 -0
- package/dist/overlay/grammar.js +223 -0
- package/dist/overlay/grammar.js.map +1 -0
- package/dist/overlay/render.d.ts +125 -0
- package/dist/overlay/render.d.ts.map +1 -0
- package/dist/overlay/render.js +439 -0
- package/dist/overlay/render.js.map +1 -0
- package/dist/overlay/styles.d.ts +41 -0
- package/dist/overlay/styles.d.ts.map +1 -0
- package/dist/overlay/styles.js +124 -0
- package/dist/overlay/styles.js.map +1 -0
- package/dist/picker/render.d.ts +80 -0
- package/dist/picker/render.d.ts.map +1 -0
- package/dist/picker/render.js +112 -0
- package/dist/picker/render.js.map +1 -0
- package/dist/picker/styles.d.ts +26 -0
- package/dist/picker/styles.d.ts.map +1 -0
- package/dist/picker/styles.js +116 -0
- package/dist/picker/styles.js.map +1 -0
- package/dist/picker/view.d.ts +135 -0
- package/dist/picker/view.d.ts.map +1 -0
- package/dist/picker/view.js +117 -0
- package/dist/picker/view.js.map +1 -0
- package/dist/picker/words.d.ts +53 -0
- package/dist/picker/words.d.ts.map +1 -0
- package/dist/picker/words.js +28 -0
- package/dist/picker/words.js.map +1 -0
- package/dist/reevaluate/render.d.ts +77 -0
- package/dist/reevaluate/render.d.ts.map +1 -0
- package/dist/reevaluate/render.js +211 -0
- package/dist/reevaluate/render.js.map +1 -0
- package/dist/reevaluate/styles.d.ts +24 -0
- package/dist/reevaluate/styles.d.ts.map +1 -0
- package/dist/reevaluate/styles.js +182 -0
- package/dist/reevaluate/styles.js.map +1 -0
- package/dist/reevaluate/view.d.ts +102 -0
- package/dist/reevaluate/view.d.ts.map +1 -0
- package/dist/reevaluate/view.js +111 -0
- package/dist/reevaluate/view.js.map +1 -0
- package/dist/reevaluate/words.d.ts +54 -0
- package/dist/reevaluate/words.d.ts.map +1 -0
- package/dist/reevaluate/words.js +29 -0
- package/dist/reevaluate/words.js.map +1 -0
- package/dist/scale/commands.d.ts +60 -0
- package/dist/scale/commands.d.ts.map +1 -0
- package/dist/scale/commands.js +49 -0
- package/dist/scale/commands.js.map +1 -0
- package/dist/scale/ladder.d.ts +141 -0
- package/dist/scale/ladder.d.ts.map +1 -0
- package/dist/scale/ladder.js +243 -0
- package/dist/scale/ladder.js.map +1 -0
- package/dist/scale/render.d.ts +85 -0
- package/dist/scale/render.d.ts.map +1 -0
- package/dist/scale/render.js +194 -0
- package/dist/scale/render.js.map +1 -0
- package/dist/scale/styles.d.ts +21 -0
- package/dist/scale/styles.d.ts.map +1 -0
- package/dist/scale/styles.js +105 -0
- package/dist/scale/styles.js.map +1 -0
- package/dist/workspace/inspector.d.ts +117 -0
- package/dist/workspace/inspector.d.ts.map +1 -0
- package/dist/workspace/inspector.js +140 -0
- package/dist/workspace/inspector.js.map +1 -0
- package/dist/workspace/rail.d.ts +122 -0
- package/dist/workspace/rail.d.ts.map +1 -0
- package/dist/workspace/rail.js +216 -0
- package/dist/workspace/rail.js.map +1 -0
- package/dist/workspace/render.d.ts +139 -0
- package/dist/workspace/render.d.ts.map +1 -0
- package/dist/workspace/render.js +366 -0
- package/dist/workspace/render.js.map +1 -0
- package/dist/workspace/selection.d.ts +92 -0
- package/dist/workspace/selection.d.ts.map +1 -0
- package/dist/workspace/selection.js +83 -0
- package/dist/workspace/selection.js.map +1 -0
- package/dist/workspace/styles.d.ts +39 -0
- package/dist/workspace/styles.d.ts.map +1 -0
- package/dist/workspace/styles.js +313 -0
- package/dist/workspace/styles.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first pass, as a pure key map: `Y` / `N` / `S`, and `⌫` to undo.
|
|
3
|
+
*
|
|
4
|
+
* §17e is explicit that the queue is keyboard-first, and about why: each answer
|
|
5
|
+
* has to take two seconds, and reaching for a pointer between questions is most
|
|
6
|
+
* of the two seconds. "Keyboard-first" here means the pointer is optional
|
|
7
|
+
* rather than assisted — the whole loop is reachable without one.
|
|
8
|
+
*
|
|
9
|
+
* ## It is `create/keys.ts`'s shape, deliberately and for its scar tissue
|
|
10
|
+
*
|
|
11
|
+
* That module took four review rounds to arrive at asking about OUR OWN
|
|
12
|
+
* interaction instead of trying to enumerate the host's other widgets, and it
|
|
13
|
+
* records the reasoning at length. This map inherits the conclusion rather than
|
|
14
|
+
* re-deriving it: a `KeyPress` carrying the modifier and composition facts, an
|
|
15
|
+
* interaction state supplied by the host, and `none` for everything else.
|
|
16
|
+
*
|
|
17
|
+
* What is NOT inherited is the interaction vocabulary. `CreateInteraction`'s
|
|
18
|
+
* `target-search` is a state of the CREATE flow, and this surface has no search
|
|
19
|
+
* box — so reusing that type would ask a host to answer a question about a flow
|
|
20
|
+
* it is not in. See {@link FirstPassInteraction}.
|
|
21
|
+
*
|
|
22
|
+
* ## `Y`, `N` and `S` are printable, which decides the whole table
|
|
23
|
+
*
|
|
24
|
+
* All three answers are letters. In any focused text control they are that
|
|
25
|
+
* control's characters, so none of them survives editing — and unlike the
|
|
26
|
+
* create flow there is no `⏎`-shaped exception, because nothing in this loop
|
|
27
|
+
* requires typing. That makes the table simpler than `create/keys.ts`'s and the
|
|
28
|
+
* simplicity is a fact about the design rather than a saving: a queue you
|
|
29
|
+
* answer by typing would not be a two-second queue.
|
|
30
|
+
*/
|
|
31
|
+
import type { KeyPress } from '../create/keys.ts';
|
|
32
|
+
import type { QueueCommand } from './queue.ts';
|
|
33
|
+
/**
|
|
34
|
+
* Where the keyboard is, in terms of THIS surface.
|
|
35
|
+
*
|
|
36
|
+
* - `queue` — the queue has the keyboard. Every binding reaches it.
|
|
37
|
+
* - `elsewhere` — anything else at all: an inline edit, a filter box, the
|
|
38
|
+
* canvas, a modal, a control this package has never heard of. Nothing
|
|
39
|
+
* reaches.
|
|
40
|
+
*
|
|
41
|
+
* TWO STATES, NOT THREE, and the missing one is the point. `create/keys.ts`
|
|
42
|
+
* needs `target-search` because its own flow contains a text box that two of
|
|
43
|
+
* its bindings must survive. This flow contains none, so a third state would be
|
|
44
|
+
* a distinction with no binding on either side of it — and every state a host
|
|
45
|
+
* has to map its world onto is a chance to map it wrongly.
|
|
46
|
+
*/
|
|
47
|
+
export type FirstPassInteraction = 'queue' | 'elsewhere';
|
|
48
|
+
/** What the shell knows when a key arrives. */
|
|
49
|
+
export interface FirstPassContext {
|
|
50
|
+
/**
|
|
51
|
+
* Which of this surface's interactions the keyboard is in.
|
|
52
|
+
*
|
|
53
|
+
* REQUIRED, NOT OPTIONAL, on the same reasoning `KeyboardContext.interaction`
|
|
54
|
+
* records: every default is wrong for some host, and the plausible default —
|
|
55
|
+
* "assume the queue" — is the one that steals keystrokes from a host that has
|
|
56
|
+
* not thought about it.
|
|
57
|
+
*/
|
|
58
|
+
readonly interaction: FirstPassInteraction;
|
|
59
|
+
/**
|
|
60
|
+
* Whether a candidate is on screen.
|
|
61
|
+
*
|
|
62
|
+
* An answer with nothing to answer is `none` rather than a command the
|
|
63
|
+
* reducer would discard, so the host does not `preventDefault()` a key it did
|
|
64
|
+
* not consume. The reducer treats the same case as a no-op independently —
|
|
65
|
+
* two guards for one property, which is right here because they protect
|
|
66
|
+
* different things: this one protects the KEY, that one protects the STATE.
|
|
67
|
+
*/
|
|
68
|
+
readonly hasCandidate: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Whether anything has been answered, i.e. whether `⌫` has work.
|
|
71
|
+
*
|
|
72
|
+
* Same reasoning: an undo with an empty history hands the key back.
|
|
73
|
+
*/
|
|
74
|
+
readonly canUndo: boolean;
|
|
75
|
+
}
|
|
76
|
+
/** What a key means. `none` leaves the key to the host. */
|
|
77
|
+
export type FirstPassIntent = {
|
|
78
|
+
readonly kind: 'none';
|
|
79
|
+
} | {
|
|
80
|
+
readonly kind: 'queue';
|
|
81
|
+
readonly command: QueueCommand;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* What one key press means, given what the shell currently holds.
|
|
85
|
+
*
|
|
86
|
+
* Total and pure. One exhaustive switch over a discriminated union, every arm a
|
|
87
|
+
* single expression, because the decision was already made by the table.
|
|
88
|
+
*/
|
|
89
|
+
export declare function firstPassIntent(press: KeyPress, context: FirstPassContext): FirstPassIntent;
|
|
90
|
+
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/firstpass/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAU,YAAY,EAAE,MAAM,YAAY,CAAC;AAEvD;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,WAAW,CAAC;AAEzD,+CAA+C;AAC/C,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;OAOG;IACH,QAAQ,CAAC,WAAW,EAAE,oBAAoB,CAAC;IAC3C;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,2DAA2D;AAC3D,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;CAAE,CAAC;AA+D/D;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,gBAAgB,GAAG,eAAe,CAuB3F"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first pass, as a pure key map: `Y` / `N` / `S`, and `⌫` to undo.
|
|
3
|
+
*
|
|
4
|
+
* §17e is explicit that the queue is keyboard-first, and about why: each answer
|
|
5
|
+
* has to take two seconds, and reaching for a pointer between questions is most
|
|
6
|
+
* of the two seconds. "Keyboard-first" here means the pointer is optional
|
|
7
|
+
* rather than assisted — the whole loop is reachable without one.
|
|
8
|
+
*
|
|
9
|
+
* ## It is `create/keys.ts`'s shape, deliberately and for its scar tissue
|
|
10
|
+
*
|
|
11
|
+
* That module took four review rounds to arrive at asking about OUR OWN
|
|
12
|
+
* interaction instead of trying to enumerate the host's other widgets, and it
|
|
13
|
+
* records the reasoning at length. This map inherits the conclusion rather than
|
|
14
|
+
* re-deriving it: a `KeyPress` carrying the modifier and composition facts, an
|
|
15
|
+
* interaction state supplied by the host, and `none` for everything else.
|
|
16
|
+
*
|
|
17
|
+
* What is NOT inherited is the interaction vocabulary. `CreateInteraction`'s
|
|
18
|
+
* `target-search` is a state of the CREATE flow, and this surface has no search
|
|
19
|
+
* box — so reusing that type would ask a host to answer a question about a flow
|
|
20
|
+
* it is not in. See {@link FirstPassInteraction}.
|
|
21
|
+
*
|
|
22
|
+
* ## `Y`, `N` and `S` are printable, which decides the whole table
|
|
23
|
+
*
|
|
24
|
+
* All three answers are letters. In any focused text control they are that
|
|
25
|
+
* control's characters, so none of them survives editing — and unlike the
|
|
26
|
+
* create flow there is no `⏎`-shaped exception, because nothing in this loop
|
|
27
|
+
* requires typing. That makes the table simpler than `create/keys.ts`'s and the
|
|
28
|
+
* simplicity is a fact about the design rather than a saving: a queue you
|
|
29
|
+
* answer by typing would not be a two-second queue.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* The vocabulary, as data.
|
|
33
|
+
*
|
|
34
|
+
* §17e names the keys `Y` / `N` / `S` and `⌫`. The letters are mnemonics in
|
|
35
|
+
* English, which is a real limitation and one this package cannot fix from
|
|
36
|
+
* here: a rebinding belongs to the host, and the reducer takes {@link Answer}
|
|
37
|
+
* values rather than key names precisely so a host can bind whatever its
|
|
38
|
+
* readers' keyboards spell without reaching into {@link ./queue.ts}.
|
|
39
|
+
*/
|
|
40
|
+
const BINDINGS = new Map([
|
|
41
|
+
['y', { action: 'answer', answer: 'apply' }],
|
|
42
|
+
['n', { action: 'answer', answer: 'reject' }],
|
|
43
|
+
['s', { action: 'answer', answer: 'skip' }],
|
|
44
|
+
// BOTH SPELLINGS OF THE DELETE KEY, for the reason `create/keys.ts` records:
|
|
45
|
+
// §17e writes it `⌫`, which is `Backspace` on the keyboards that have it and
|
|
46
|
+
// `Delete` on those that do not — most notably Apple's. Binding one of the
|
|
47
|
+
// two would make "no pointer" false on whichever hardware got the other.
|
|
48
|
+
['backspace', { action: 'undo' }],
|
|
49
|
+
['delete', { action: 'undo' }],
|
|
50
|
+
]);
|
|
51
|
+
const NONE = Object.freeze({ kind: 'none' });
|
|
52
|
+
/**
|
|
53
|
+
* The press type is `create/keys.ts`'s, imported rather than redeclared.
|
|
54
|
+
*
|
|
55
|
+
* A KEY PRESS IS A PLATFORM FACT, not a fact about either flow: the fields are
|
|
56
|
+
* `KeyboardEvent`'s, fixed by the DOM, and identical here for reasons that are
|
|
57
|
+
* identical too — a bare key name cannot tell `Y` from `Cmd+Y`, an input method
|
|
58
|
+
* owns every printable key while composing, and an auto-repeat is not a second
|
|
59
|
+
* act. Two structurally identical declarations of it would be the drifting
|
|
60
|
+
* second implementation this package family removes everywhere it appears, and
|
|
61
|
+
* they would drift in the worst possible place: the day the DOM grows a field
|
|
62
|
+
* one flow must honour, only one of the two would learn about it.
|
|
63
|
+
*
|
|
64
|
+
* So the shared definition is imported across, and the package's surface keeps
|
|
65
|
+
* exporting it exactly once, from where it is declared — a host wiring both
|
|
66
|
+
* surfaces holds one `KeyPress` and forwards the same event to either map.
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* Whether this press is a chord that belongs to the platform.
|
|
70
|
+
*
|
|
71
|
+
* `shiftKey` IS DELIBERATELY ABSENT — §17e names its bindings in capitals, and
|
|
72
|
+
* `Shift+y` is how a keyboard reports `Y`, so treating shift as a modifier
|
|
73
|
+
* would unbind the very keys the design specifies. Folded by {@link normalize}.
|
|
74
|
+
*/
|
|
75
|
+
function chorded(press) {
|
|
76
|
+
return press.ctrlKey === true || press.metaKey === true || press.altKey === true;
|
|
77
|
+
}
|
|
78
|
+
/** Fold `KeyboardEvent.key` into the table's spelling — see `create/keys.ts`. */
|
|
79
|
+
function normalize(key) {
|
|
80
|
+
return key.toLowerCase();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* What one key press means, given what the shell currently holds.
|
|
84
|
+
*
|
|
85
|
+
* Total and pure. One exhaustive switch over a discriminated union, every arm a
|
|
86
|
+
* single expression, because the decision was already made by the table.
|
|
87
|
+
*/
|
|
88
|
+
export function firstPassIntent(press, context) {
|
|
89
|
+
// THE PRESS ITSELF DISQUALIFIES IT, before any lookup. A chord and a
|
|
90
|
+
// composition belong to someone else; a repeat is not a fresh act at all.
|
|
91
|
+
if (chorded(press) || press.isComposing === true || press.repeat === true)
|
|
92
|
+
return NONE;
|
|
93
|
+
// NOTHING REACHES FROM `elsewhere`. Written as an early return rather than a
|
|
94
|
+
// decision table on the binding because — unlike the create map — no binding
|
|
95
|
+
// here survives another control's focus, so a per-binding column would have
|
|
96
|
+
// one value in every row. `create/keys.ts` earns its table; this would be
|
|
97
|
+
// ceremony asserting a distinction the design does not make.
|
|
98
|
+
if (context.interaction !== 'queue')
|
|
99
|
+
return NONE;
|
|
100
|
+
const binding = BINDINGS.get(normalize(press.key));
|
|
101
|
+
if (binding === undefined)
|
|
102
|
+
return NONE;
|
|
103
|
+
switch (binding.action) {
|
|
104
|
+
case 'answer':
|
|
105
|
+
return context.hasCandidate
|
|
106
|
+
? { kind: 'queue', command: { kind: 'answer', answer: binding.answer } }
|
|
107
|
+
: NONE;
|
|
108
|
+
case 'undo':
|
|
109
|
+
return context.canUndo ? { kind: 'queue', command: { kind: 'undo' } } : NONE;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../../src/firstpass/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AA4DH;;;;;;;;GAQG;AACH,MAAM,QAAQ,GAAiC,IAAI,GAAG,CAAkB;IACtE,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;IAC5C,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;IAC7C,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAC3C,6EAA6E;IAC7E,6EAA6E;IAC7E,2EAA2E;IAC3E,yEAAyE;IACzE,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjC,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;CAC/B,CAAC,CAAC;AAEH,MAAM,IAAI,GAAoB,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;AAE9D;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;GAMG;AACH,SAAS,OAAO,CAAC,KAAe;IAC9B,OAAO,KAAK,CAAC,OAAO,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI,CAAC;AACnF,CAAC;AAED,iFAAiF;AACjF,SAAS,SAAS,CAAC,GAAW;IAC5B,OAAO,GAAG,CAAC,WAAW,EAAE,CAAC;AAC3B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,KAAe,EAAE,OAAyB;IACxE,qEAAqE;IACrE,0EAA0E;IAC1E,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,WAAW,KAAK,IAAI,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAEvF,6EAA6E;IAC7E,6EAA6E;IAC7E,4EAA4E;IAC5E,0EAA0E;IAC1E,6DAA6D;IAC7D,IAAI,OAAO,CAAC,WAAW,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IAEjD,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACnD,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAEvC,QAAQ,OAAO,CAAC,MAAM,EAAE,CAAC;QACvB,KAAK,QAAQ;YACX,OAAO,OAAO,CAAC,YAAY;gBACzB,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,EAAE;gBACxE,CAAC,CAAC,IAAI,CAAC;QACX,KAAK,MAAM;YACT,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACjF,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first-pass review queue: one candidate, one keystroke, one answer.
|
|
3
|
+
*
|
|
4
|
+
* §17e's loop is `Y` / `N` / `S` with `⌫` to undo, over candidates the host
|
|
5
|
+
* found. This module is the whole of that as a reducer — the same shape
|
|
6
|
+
* `create/draft.ts` and `scale/commands.ts` already use here, so a host
|
|
7
|
+
* reducing one reduces this without learning a second protocol.
|
|
8
|
+
*
|
|
9
|
+
* ## Nothing is applied without a keystroke, and the shape is what guarantees it
|
|
10
|
+
*
|
|
11
|
+
* §17e gives the reason and it is worth quoting rather than paraphrasing: "A
|
|
12
|
+
* wrong `duplicate-of` silently removes real work from the order, so it always
|
|
13
|
+
* costs one keystroke of consent."
|
|
14
|
+
*
|
|
15
|
+
* A `duplicate-of` excludes an issue from the order ENTIRELY. Applied without
|
|
16
|
+
* consent, that is work vanishing from the backlog while looking handled — the
|
|
17
|
+
* dead-duplicate audit class arriving through automation instead of decay.
|
|
18
|
+
*
|
|
19
|
+
* So consent is structural rather than promised: the ONLY thing in this module
|
|
20
|
+
* that returns a non-null proposal is the `answer` command carrying
|
|
21
|
+
* {@link Answer} `apply`, and nothing else here constructs one at all. Advancing
|
|
22
|
+
* the queue, loading it, rendering it and undoing all emit `null` because there
|
|
23
|
+
* is no code path on which they could emit anything else. `queue.test.ts` pins
|
|
24
|
+
* it from the other side — a whole queue driven to exhaustion by `reject` and
|
|
25
|
+
* `skip` dispatches nothing — and `render.test.ts` pins the surface: drawing a
|
|
26
|
+
* candidate is not answering it.
|
|
27
|
+
*
|
|
28
|
+
* ## Progress is bounded by CANDIDATES, and never by backlog size
|
|
29
|
+
*
|
|
30
|
+
* "Progress is bounded by candidates found, not backlog size, so first pass has
|
|
31
|
+
* an end. 100% encoded is never the goal and the workspace never implies it is
|
|
32
|
+
* — most issues legitimately have no relationships."
|
|
33
|
+
*
|
|
34
|
+
* The denominator is therefore {@link QueueState.candidates}`.length` and this
|
|
35
|
+
* module is never told how many issues exist — it cannot compute the wrong
|
|
36
|
+
* denominator because it does not hold the number that would be wrong. An owner
|
|
37
|
+
* with 248 legitimately-isolated issues is not shown "20% done"; they are shown
|
|
38
|
+
* a queue of 64 questions with an end, which is the difference between a
|
|
39
|
+
* surface that gets finished and one that gets abandoned.
|
|
40
|
+
*
|
|
41
|
+
* ## Undo returns the QUESTION; it does not retract the WRITE
|
|
42
|
+
*
|
|
43
|
+
* `⌫` steps the cursor back and un-answers the candidate. What it cannot do is
|
|
44
|
+
* un-dispatch a proposal that already went to the store — that is the store's
|
|
45
|
+
* undo, over its own mutation set, and a second retraction path out here would
|
|
46
|
+
* be free to disagree with it about what undoing a create means.
|
|
47
|
+
*
|
|
48
|
+
* So an undo that withdraws an `apply` REPORTS the withdrawal
|
|
49
|
+
* ({@link QueueResult.withdrawn}) and the host routes it to the store. The
|
|
50
|
+
* queue's own state is honest either way: the candidate is unanswered again,
|
|
51
|
+
* which is what the reader just asked for.
|
|
52
|
+
*/
|
|
53
|
+
import type { Proposal } from '@issuegraph/store';
|
|
54
|
+
import type { Candidate, CandidateId } from './candidates.ts';
|
|
55
|
+
/**
|
|
56
|
+
* What an owner said about a candidate.
|
|
57
|
+
*
|
|
58
|
+
* Three, closed by §17e, and named for the DECISION rather than for the key —
|
|
59
|
+
* `apply` rather than `yes`, because the keyboard spelling belongs to
|
|
60
|
+
* {@link ./keys.ts} and a rebinding must not have to reach in here.
|
|
61
|
+
*
|
|
62
|
+
* - `apply` — yes, create it. The one answer that emits.
|
|
63
|
+
* - `reject` — no, this is not a relationship. Answered, and gone.
|
|
64
|
+
* - `skip` — not now. Answered for the purpose of PROGRESS, because the owner
|
|
65
|
+
* did the work of looking; deferring it into a second pass would make the
|
|
66
|
+
* progress bar a lie in the direction that matters, by never letting a queue
|
|
67
|
+
* with hard cases in it end.
|
|
68
|
+
*/
|
|
69
|
+
export type Answer = 'apply' | 'reject' | 'skip';
|
|
70
|
+
/** A candidate, and what was said about it. */
|
|
71
|
+
export interface Answered {
|
|
72
|
+
readonly candidate: Candidate;
|
|
73
|
+
readonly answer: Answer;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The queue.
|
|
77
|
+
*
|
|
78
|
+
* `cursor` indexes {@link candidates} and is the number of answers given, which
|
|
79
|
+
* is not a coincidence to be maintained but the reason there is no second
|
|
80
|
+
* field: answers are given in order and undone in order, so the two cannot
|
|
81
|
+
* drift. `answers[i]` is the answer to `candidates[i]`.
|
|
82
|
+
*/
|
|
83
|
+
export interface QueueState {
|
|
84
|
+
/** Everything the host found. The denominator, and never anything else. */
|
|
85
|
+
readonly candidates: readonly Candidate[];
|
|
86
|
+
/** How far in. Equal to `answers.length`; past the end means finished. */
|
|
87
|
+
readonly cursor: number;
|
|
88
|
+
/** What was said, oldest first. */
|
|
89
|
+
readonly answers: readonly Answered[];
|
|
90
|
+
}
|
|
91
|
+
/** A queue over the candidates a host supplied, with nothing answered yet. */
|
|
92
|
+
export declare function openQueue(candidates: readonly Candidate[]): QueueState;
|
|
93
|
+
/**
|
|
94
|
+
* One act.
|
|
95
|
+
*
|
|
96
|
+
* `answer` carries the DECISION and not the candidate: the queue shows exactly
|
|
97
|
+
* one candidate, so which one is being answered is the queue's own state rather
|
|
98
|
+
* than something a caller can get wrong. A command naming a candidate would
|
|
99
|
+
* admit answering one that is not on screen, which is consent for a question
|
|
100
|
+
* nobody was asked.
|
|
101
|
+
*/
|
|
102
|
+
export type QueueCommand = {
|
|
103
|
+
readonly kind: 'answer';
|
|
104
|
+
readonly answer: Answer;
|
|
105
|
+
} | {
|
|
106
|
+
readonly kind: 'undo';
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* The next queue, what this transition emitted, and what it took back.
|
|
110
|
+
*
|
|
111
|
+
* Three fields rather than a union because a host wires all three once —
|
|
112
|
+
* `state` always, `proposal` to the store, `withdrawn` to the store's undo —
|
|
113
|
+
* and a union would make the ordinary reduction a switch over cases that mostly
|
|
114
|
+
* do the same thing. Same reasoning `CreateResult` records for its pair.
|
|
115
|
+
*/
|
|
116
|
+
export interface QueueResult {
|
|
117
|
+
readonly state: QueueState;
|
|
118
|
+
/** Non-null on exactly one transition: an `apply`. */
|
|
119
|
+
readonly proposal: Proposal | null;
|
|
120
|
+
/**
|
|
121
|
+
* The answer an `undo` took back, or `null`.
|
|
122
|
+
*
|
|
123
|
+
* A withdrawn `apply` is the one the host must act on — its proposal is
|
|
124
|
+
* already at the store. A withdrawn `reject` or `skip` dispatched nothing, so
|
|
125
|
+
* there is nothing to undo beyond the queue position, and it is reported
|
|
126
|
+
* anyway so the host has one code path rather than two.
|
|
127
|
+
*/
|
|
128
|
+
readonly withdrawn: Answered | null;
|
|
129
|
+
}
|
|
130
|
+
/** Progress, as §17e defines it. */
|
|
131
|
+
export interface QueueProgress {
|
|
132
|
+
/** Answers given. */
|
|
133
|
+
readonly answered: number;
|
|
134
|
+
/**
|
|
135
|
+
* Candidates FOUND. The denominator, and the whole point of this type.
|
|
136
|
+
*
|
|
137
|
+
* Named `found` rather than `total` deliberately: `total` is the word that
|
|
138
|
+
* invites a reader to reach for the backlog size, and this number is not
|
|
139
|
+
* that. See the module header.
|
|
140
|
+
*/
|
|
141
|
+
readonly found: number;
|
|
142
|
+
/** Still to answer. */
|
|
143
|
+
readonly remaining: number;
|
|
144
|
+
/** Nothing left. A queue over no candidates is finished immediately. */
|
|
145
|
+
readonly finished: boolean;
|
|
146
|
+
}
|
|
147
|
+
export declare function queueProgress(state: QueueState): QueueProgress;
|
|
148
|
+
/**
|
|
149
|
+
* The candidate on screen, or `null` when the queue is finished.
|
|
150
|
+
*
|
|
151
|
+
* `null` rather than a sentinel candidate: "there is nothing to answer" is a
|
|
152
|
+
* genuinely different state from "here is a question", and a placeholder would
|
|
153
|
+
* be answerable.
|
|
154
|
+
*/
|
|
155
|
+
export declare function currentCandidate(state: QueueState): Candidate | null;
|
|
156
|
+
/**
|
|
157
|
+
* Apply one command. Total, pure, and never mutates what it is given.
|
|
158
|
+
*
|
|
159
|
+
* An exhaustive switch over a discriminated union — the one branching form
|
|
160
|
+
* `AGENTS.md`'s boundary rule leaves open, and the shape every reducer in this
|
|
161
|
+
* package already uses. Adding a command without a case fails the build.
|
|
162
|
+
*/
|
|
163
|
+
export declare function queueReducer(state: QueueState, command: QueueCommand): QueueResult;
|
|
164
|
+
/**
|
|
165
|
+
* The candidates an owner deferred, in the order they were deferred.
|
|
166
|
+
*
|
|
167
|
+
* §17e's `S` is "not now" rather than "no", so the deferred set is the thing a
|
|
168
|
+
* second pass is built from — and it is DERIVED here rather than accumulated in
|
|
169
|
+
* {@link QueueState}, so an undo cannot leave it disagreeing with the answers
|
|
170
|
+
* it summarises. That is the same reason `queueProgress` derives its counts:
|
|
171
|
+
* two fields recording one fact is one fact that can be wrong.
|
|
172
|
+
*/
|
|
173
|
+
export declare function skippedCandidates(state: QueueState): readonly Candidate[];
|
|
174
|
+
/**
|
|
175
|
+
* Whether a candidate has already been answered in this queue.
|
|
176
|
+
*
|
|
177
|
+
* For a host merging a fresh candidate set into an open queue — a second
|
|
178
|
+
* detector finishing late, say. It asks by {@link CandidateId} because that is
|
|
179
|
+
* the identity the HOST minted, for exactly the reason `candidates.ts` records:
|
|
180
|
+
* two findings about the same pair are not necessarily the same finding.
|
|
181
|
+
*/
|
|
182
|
+
export declare function isAnswered(state: QueueState, id: CandidateId): boolean;
|
|
183
|
+
//# sourceMappingURL=queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../../src/firstpass/queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAElD,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9D;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEjD,+CAA+C;AAC/C,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,CAAC;IAC1C,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,mCAAmC;IACnC,QAAQ,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,CAAC;CACvC;AAED,8EAA8E;AAC9E,wBAAgB,SAAS,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,GAAG,UAAU,CAEtE;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9B;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IACnC;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,QAAQ,GAAG,IAAI,CAAC;CACrC;AAED,oCAAoC;AACpC,MAAM,WAAW,aAAa;IAC5B,qBAAqB;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uBAAuB;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,aAAa,CAI9D;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,IAAI,CAEpE;AAeD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,GAAG,WAAW,CAyClF;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,SAAS,EAAE,CAIzE;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,WAAW,GAAG,OAAO,CAEtE"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first-pass review queue: one candidate, one keystroke, one answer.
|
|
3
|
+
*
|
|
4
|
+
* §17e's loop is `Y` / `N` / `S` with `⌫` to undo, over candidates the host
|
|
5
|
+
* found. This module is the whole of that as a reducer — the same shape
|
|
6
|
+
* `create/draft.ts` and `scale/commands.ts` already use here, so a host
|
|
7
|
+
* reducing one reduces this without learning a second protocol.
|
|
8
|
+
*
|
|
9
|
+
* ## Nothing is applied without a keystroke, and the shape is what guarantees it
|
|
10
|
+
*
|
|
11
|
+
* §17e gives the reason and it is worth quoting rather than paraphrasing: "A
|
|
12
|
+
* wrong `duplicate-of` silently removes real work from the order, so it always
|
|
13
|
+
* costs one keystroke of consent."
|
|
14
|
+
*
|
|
15
|
+
* A `duplicate-of` excludes an issue from the order ENTIRELY. Applied without
|
|
16
|
+
* consent, that is work vanishing from the backlog while looking handled — the
|
|
17
|
+
* dead-duplicate audit class arriving through automation instead of decay.
|
|
18
|
+
*
|
|
19
|
+
* So consent is structural rather than promised: the ONLY thing in this module
|
|
20
|
+
* that returns a non-null proposal is the `answer` command carrying
|
|
21
|
+
* {@link Answer} `apply`, and nothing else here constructs one at all. Advancing
|
|
22
|
+
* the queue, loading it, rendering it and undoing all emit `null` because there
|
|
23
|
+
* is no code path on which they could emit anything else. `queue.test.ts` pins
|
|
24
|
+
* it from the other side — a whole queue driven to exhaustion by `reject` and
|
|
25
|
+
* `skip` dispatches nothing — and `render.test.ts` pins the surface: drawing a
|
|
26
|
+
* candidate is not answering it.
|
|
27
|
+
*
|
|
28
|
+
* ## Progress is bounded by CANDIDATES, and never by backlog size
|
|
29
|
+
*
|
|
30
|
+
* "Progress is bounded by candidates found, not backlog size, so first pass has
|
|
31
|
+
* an end. 100% encoded is never the goal and the workspace never implies it is
|
|
32
|
+
* — most issues legitimately have no relationships."
|
|
33
|
+
*
|
|
34
|
+
* The denominator is therefore {@link QueueState.candidates}`.length` and this
|
|
35
|
+
* module is never told how many issues exist — it cannot compute the wrong
|
|
36
|
+
* denominator because it does not hold the number that would be wrong. An owner
|
|
37
|
+
* with 248 legitimately-isolated issues is not shown "20% done"; they are shown
|
|
38
|
+
* a queue of 64 questions with an end, which is the difference between a
|
|
39
|
+
* surface that gets finished and one that gets abandoned.
|
|
40
|
+
*
|
|
41
|
+
* ## Undo returns the QUESTION; it does not retract the WRITE
|
|
42
|
+
*
|
|
43
|
+
* `⌫` steps the cursor back and un-answers the candidate. What it cannot do is
|
|
44
|
+
* un-dispatch a proposal that already went to the store — that is the store's
|
|
45
|
+
* undo, over its own mutation set, and a second retraction path out here would
|
|
46
|
+
* be free to disagree with it about what undoing a create means.
|
|
47
|
+
*
|
|
48
|
+
* So an undo that withdraws an `apply` REPORTS the withdrawal
|
|
49
|
+
* ({@link QueueResult.withdrawn}) and the host routes it to the store. The
|
|
50
|
+
* queue's own state is honest either way: the candidate is unanswered again,
|
|
51
|
+
* which is what the reader just asked for.
|
|
52
|
+
*/
|
|
53
|
+
/** A queue over the candidates a host supplied, with nothing answered yet. */
|
|
54
|
+
export function openQueue(candidates) {
|
|
55
|
+
return { candidates, cursor: 0, answers: [] };
|
|
56
|
+
}
|
|
57
|
+
export function queueProgress(state) {
|
|
58
|
+
const found = state.candidates.length;
|
|
59
|
+
const answered = state.answers.length;
|
|
60
|
+
return { answered, found, remaining: found - answered, finished: answered >= found };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The candidate on screen, or `null` when the queue is finished.
|
|
64
|
+
*
|
|
65
|
+
* `null` rather than a sentinel candidate: "there is nothing to answer" is a
|
|
66
|
+
* genuinely different state from "here is a question", and a placeholder would
|
|
67
|
+
* be answerable.
|
|
68
|
+
*/
|
|
69
|
+
export function currentCandidate(state) {
|
|
70
|
+
return state.candidates[state.cursor] ?? null;
|
|
71
|
+
}
|
|
72
|
+
const NOTHING = Object.freeze({ proposal: null, withdrawn: null });
|
|
73
|
+
/**
|
|
74
|
+
* The proposal an `apply` stands for.
|
|
75
|
+
*
|
|
76
|
+
* ITS OWN FUNCTION, so the module has exactly one place that builds a
|
|
77
|
+
* `Proposal` and the consent claim in the header is checkable by reading rather
|
|
78
|
+
* than by trusting. It is also the only reason `Proposal` is imported at all.
|
|
79
|
+
*/
|
|
80
|
+
function proposalFor(candidate) {
|
|
81
|
+
return { op: 'create', kind: candidate.kind, from: candidate.from, to: candidate.to };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Apply one command. Total, pure, and never mutates what it is given.
|
|
85
|
+
*
|
|
86
|
+
* An exhaustive switch over a discriminated union — the one branching form
|
|
87
|
+
* `AGENTS.md`'s boundary rule leaves open, and the shape every reducer in this
|
|
88
|
+
* package already uses. Adding a command without a case fails the build.
|
|
89
|
+
*/
|
|
90
|
+
export function queueReducer(state, command) {
|
|
91
|
+
switch (command.kind) {
|
|
92
|
+
case 'answer': {
|
|
93
|
+
const candidate = currentCandidate(state);
|
|
94
|
+
// ANSWERING A FINISHED QUEUE IS A NO-OP, not an error and not a wrap-round.
|
|
95
|
+
// A key press racing the last answer is ordinary — the reader is going
|
|
96
|
+
// fast, which is the design target — and the alternatives are both worse
|
|
97
|
+
// than doing nothing: throwing turns a fast reader into an error dialog,
|
|
98
|
+
// and wrapping re-asks a question they already answered.
|
|
99
|
+
if (candidate === null)
|
|
100
|
+
return { state, ...NOTHING };
|
|
101
|
+
const answered = { candidate, answer: command.answer };
|
|
102
|
+
const next = {
|
|
103
|
+
...state,
|
|
104
|
+
cursor: state.cursor + 1,
|
|
105
|
+
answers: [...state.answers, answered],
|
|
106
|
+
};
|
|
107
|
+
// THE ONLY EMISSION IN THIS MODULE. `reject` and `skip` advance and say
|
|
108
|
+
// nothing, which is what makes "no candidate is applied without a
|
|
109
|
+
// keystroke" a property of the shape rather than a promise.
|
|
110
|
+
return {
|
|
111
|
+
state: next,
|
|
112
|
+
proposal: command.answer === 'apply' ? proposalFor(candidate) : null,
|
|
113
|
+
withdrawn: null,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
case 'undo': {
|
|
117
|
+
const last = state.answers[state.answers.length - 1];
|
|
118
|
+
// Nothing answered is nothing to undo. Same reasoning as above: the key
|
|
119
|
+
// is handed back rather than made into an error.
|
|
120
|
+
if (last === undefined)
|
|
121
|
+
return { state, ...NOTHING };
|
|
122
|
+
return {
|
|
123
|
+
state: {
|
|
124
|
+
...state,
|
|
125
|
+
cursor: state.cursor - 1,
|
|
126
|
+
answers: state.answers.slice(0, -1),
|
|
127
|
+
},
|
|
128
|
+
proposal: null,
|
|
129
|
+
withdrawn: last,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The candidates an owner deferred, in the order they were deferred.
|
|
136
|
+
*
|
|
137
|
+
* §17e's `S` is "not now" rather than "no", so the deferred set is the thing a
|
|
138
|
+
* second pass is built from — and it is DERIVED here rather than accumulated in
|
|
139
|
+
* {@link QueueState}, so an undo cannot leave it disagreeing with the answers
|
|
140
|
+
* it summarises. That is the same reason `queueProgress` derives its counts:
|
|
141
|
+
* two fields recording one fact is one fact that can be wrong.
|
|
142
|
+
*/
|
|
143
|
+
export function skippedCandidates(state) {
|
|
144
|
+
return state.answers
|
|
145
|
+
.filter((answered) => answered.answer === 'skip')
|
|
146
|
+
.map((answered) => answered.candidate);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Whether a candidate has already been answered in this queue.
|
|
150
|
+
*
|
|
151
|
+
* For a host merging a fresh candidate set into an open queue — a second
|
|
152
|
+
* detector finishing late, say. It asks by {@link CandidateId} because that is
|
|
153
|
+
* the identity the HOST minted, for exactly the reason `candidates.ts` records:
|
|
154
|
+
* two findings about the same pair are not necessarily the same finding.
|
|
155
|
+
*/
|
|
156
|
+
export function isAnswered(state, id) {
|
|
157
|
+
return state.answers.some((answered) => answered.candidate.id === id);
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue.js","sourceRoot":"","sources":["../../src/firstpass/queue.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first-pass queue, as markup.
|
|
3
|
+
*
|
|
4
|
+
* ## Drawing a candidate answers nothing
|
|
5
|
+
*
|
|
6
|
+
* The property §17e turns on, and the one this file most has to be trusted
|
|
7
|
+
* with: rendering emits no proposal, wires no listener and dispatches nothing.
|
|
8
|
+
* It publishes `data-ig-answer` on three buttons and stops. The consent
|
|
9
|
+
* §17e requires — "it always costs one keystroke" — survives because the
|
|
10
|
+
* only thing in this package that builds a `Proposal` from a candidate is
|
|
11
|
+
* `queue.ts`'s `apply` arm, and nothing here calls it.
|
|
12
|
+
*
|
|
13
|
+
* `render.test.ts` asserts it from the outside: a rendered candidate produces
|
|
14
|
+
* no proposal, because there is no return channel on which it could.
|
|
15
|
+
*
|
|
16
|
+
* ## Every readable byte comes from the host or from the document
|
|
17
|
+
*
|
|
18
|
+
* The same total claim `picker/render.ts` makes, and it is stricter here than
|
|
19
|
+
* it looks, because the evidence text is host prose flowing through a package
|
|
20
|
+
* surface. This module never reads it, never truncates it and never summarises
|
|
21
|
+
* it — it places it, with the evidence `token` beside it as a data attribute so
|
|
22
|
+
* a host can style a class of reason and a test can assert one without matching
|
|
23
|
+
* on words.
|
|
24
|
+
*
|
|
25
|
+
* ## Progress is drawn as a number the host worded, not a bar
|
|
26
|
+
*
|
|
27
|
+
* §17e: "100% encoded is never the goal and the workspace never implies it is."
|
|
28
|
+
* A progress BAR implies a target by its geometry — an empty tail is a thing
|
|
29
|
+
* left undone — and no wording can undo that. So the surface publishes the two
|
|
30
|
+
* counts as attributes and renders the host's sentence, and a host that wants a
|
|
31
|
+
* bar draws one knowing what it is claiming. `<progress>` is deliberately not
|
|
32
|
+
* used for the same reason.
|
|
33
|
+
*
|
|
34
|
+
* ## It names itself without claiming a heading level
|
|
35
|
+
*
|
|
36
|
+
* Same call `picker/render.ts` records: a heading would be a claim about the
|
|
37
|
+
* HOST's document outline — whether this surface is a section of something, and
|
|
38
|
+
* at what depth — which a package rendered into an unknown page cannot make.
|
|
39
|
+
*
|
|
40
|
+
* That refusal used to be the whole of it, and it left the root `<section>`
|
|
41
|
+
* with no accessible name at all: a screen-reader user navigating by landmarks
|
|
42
|
+
* met an unnamed generic region and could not tell it from any other. The
|
|
43
|
+
* refusal was right and the silence that followed it was not, so the region now
|
|
44
|
+
* carries an `aria-label` from {@link ./words.ts FirstPassWords} — which names
|
|
45
|
+
* the surface without asserting anything about where it sits in an outline. A
|
|
46
|
+
* host that wants it in the outline still wraps it in a heading of its own
|
|
47
|
+
* choosing.
|
|
48
|
+
*/
|
|
49
|
+
import { type Theme } from '@issuegraph/viewer';
|
|
50
|
+
import { type FirstPassView } from './view.ts';
|
|
51
|
+
import type { QueueState } from './queue.ts';
|
|
52
|
+
import type { FirstPassWords } from './words.ts';
|
|
53
|
+
/** The attribute a shell reads to know which answer a control stands for. */
|
|
54
|
+
export declare const ANSWER_ATTRIBUTE = "data-ig-answer";
|
|
55
|
+
/**
|
|
56
|
+
* The attribute on the undo control.
|
|
57
|
+
*
|
|
58
|
+
* ITS OWN ATTRIBUTE, not `data-ig-answer="undo"`. {@link ./queue.ts Answer} is
|
|
59
|
+
* a closed union of three, and undo is not one of them — so a shell reading
|
|
60
|
+
* `data-ig-answer` and switching over `Answer` would be handed a value outside
|
|
61
|
+
* the type it was told to expect. `data-ig-command` is the spelling
|
|
62
|
+
* `picker/render.ts` already uses for a control that is not a choice.
|
|
63
|
+
*/
|
|
64
|
+
export declare const COMMAND_ATTRIBUTE = "data-ig-command";
|
|
65
|
+
/** The attribute carrying an evidence item's machine-readable token. */
|
|
66
|
+
export declare const EVIDENCE_TOKEN_ATTRIBUTE = "data-ig-evidence";
|
|
67
|
+
/** Answers given, on the root. */
|
|
68
|
+
export declare const ANSWERED_ATTRIBUTE = "data-ig-answered";
|
|
69
|
+
/** Candidates found — the denominator, on the root. */
|
|
70
|
+
export declare const FOUND_ATTRIBUTE = "data-ig-found";
|
|
71
|
+
export interface FirstPassOptions {
|
|
72
|
+
/** The words. Required — see {@link ./words.ts FirstPassWords} for why. */
|
|
73
|
+
readonly words: FirstPassWords;
|
|
74
|
+
readonly theme?: Theme | undefined;
|
|
75
|
+
/** The selector the theme's custom properties are written onto. */
|
|
76
|
+
readonly themeSelector?: string | undefined;
|
|
77
|
+
}
|
|
78
|
+
export interface FirstPassResult {
|
|
79
|
+
readonly view: FirstPassView;
|
|
80
|
+
/** The question, its evidence, the answers and the progress, under one root. */
|
|
81
|
+
readonly markup: string;
|
|
82
|
+
/**
|
|
83
|
+
* The theme and this surface's own stylesheet. Install both.
|
|
84
|
+
*
|
|
85
|
+
* The VIEWER's stylesheet is deliberately not among them, on the same
|
|
86
|
+
* reasoning `picker/render.ts` records: this surface draws no viewer
|
|
87
|
+
* elements, so shipping the canvas sheet with it would give a host that
|
|
88
|
+
* installs the queue alone a stylesheet whose every selector matches nothing.
|
|
89
|
+
*/
|
|
90
|
+
readonly styles: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Draw the queue.
|
|
94
|
+
*
|
|
95
|
+
* Takes a {@link QueueState} — never a store and never a source. A host hands
|
|
96
|
+
* the proposal a reduction produced to `Store.propose`, which is the only thing
|
|
97
|
+
* in the family that dispatches.
|
|
98
|
+
*/
|
|
99
|
+
export declare function renderFirstPass(state: QueueState, options: FirstPassOptions): FirstPassResult;
|
|
100
|
+
//# sourceMappingURL=render.d.ts.map
|