@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.
Files changed (143) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +238 -0
  3. package/dist/audit/findings.d.ts +225 -0
  4. package/dist/audit/findings.d.ts.map +1 -0
  5. package/dist/audit/findings.js +348 -0
  6. package/dist/audit/findings.js.map +1 -0
  7. package/dist/audit/styles.d.ts +37 -0
  8. package/dist/audit/styles.d.ts.map +1 -0
  9. package/dist/audit/styles.js +72 -0
  10. package/dist/audit/styles.js.map +1 -0
  11. package/dist/audit/surface.d.ts +155 -0
  12. package/dist/audit/surface.d.ts.map +1 -0
  13. package/dist/audit/surface.js +202 -0
  14. package/dist/audit/surface.js.map +1 -0
  15. package/dist/create/draft.d.ts +119 -0
  16. package/dist/create/draft.d.ts.map +1 -0
  17. package/dist/create/draft.js +114 -0
  18. package/dist/create/draft.js.map +1 -0
  19. package/dist/create/keys.d.ts +225 -0
  20. package/dist/create/keys.d.ts.map +1 -0
  21. package/dist/create/keys.js +228 -0
  22. package/dist/create/keys.js.map +1 -0
  23. package/dist/create/placement.d.ts +69 -0
  24. package/dist/create/placement.d.ts.map +1 -0
  25. package/dist/create/placement.js +76 -0
  26. package/dist/create/placement.js.map +1 -0
  27. package/dist/firstpass/batch.d.ts +190 -0
  28. package/dist/firstpass/batch.d.ts.map +1 -0
  29. package/dist/firstpass/batch.js +143 -0
  30. package/dist/firstpass/batch.js.map +1 -0
  31. package/dist/firstpass/candidates.d.ts +128 -0
  32. package/dist/firstpass/candidates.d.ts.map +1 -0
  33. package/dist/firstpass/candidates.js +39 -0
  34. package/dist/firstpass/candidates.js.map +1 -0
  35. package/dist/firstpass/keys.d.ts +90 -0
  36. package/dist/firstpass/keys.d.ts.map +1 -0
  37. package/dist/firstpass/keys.js +112 -0
  38. package/dist/firstpass/keys.js.map +1 -0
  39. package/dist/firstpass/queue.d.ts +183 -0
  40. package/dist/firstpass/queue.d.ts.map +1 -0
  41. package/dist/firstpass/queue.js +159 -0
  42. package/dist/firstpass/queue.js.map +1 -0
  43. package/dist/firstpass/render.d.ts +100 -0
  44. package/dist/firstpass/render.d.ts.map +1 -0
  45. package/dist/firstpass/render.js +216 -0
  46. package/dist/firstpass/render.js.map +1 -0
  47. package/dist/firstpass/styles.d.ts +36 -0
  48. package/dist/firstpass/styles.d.ts.map +1 -0
  49. package/dist/firstpass/styles.js +148 -0
  50. package/dist/firstpass/styles.js.map +1 -0
  51. package/dist/firstpass/view.d.ts +82 -0
  52. package/dist/firstpass/view.d.ts.map +1 -0
  53. package/dist/firstpass/view.js +60 -0
  54. package/dist/firstpass/view.js.map +1 -0
  55. package/dist/firstpass/words.d.ts +105 -0
  56. package/dist/firstpass/words.d.ts.map +1 -0
  57. package/dist/firstpass/words.js +28 -0
  58. package/dist/firstpass/words.js.map +1 -0
  59. package/dist/index.d.ts +150 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +147 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/overlay/grammar.d.ts +259 -0
  64. package/dist/overlay/grammar.d.ts.map +1 -0
  65. package/dist/overlay/grammar.js +223 -0
  66. package/dist/overlay/grammar.js.map +1 -0
  67. package/dist/overlay/render.d.ts +125 -0
  68. package/dist/overlay/render.d.ts.map +1 -0
  69. package/dist/overlay/render.js +439 -0
  70. package/dist/overlay/render.js.map +1 -0
  71. package/dist/overlay/styles.d.ts +41 -0
  72. package/dist/overlay/styles.d.ts.map +1 -0
  73. package/dist/overlay/styles.js +124 -0
  74. package/dist/overlay/styles.js.map +1 -0
  75. package/dist/picker/render.d.ts +80 -0
  76. package/dist/picker/render.d.ts.map +1 -0
  77. package/dist/picker/render.js +112 -0
  78. package/dist/picker/render.js.map +1 -0
  79. package/dist/picker/styles.d.ts +26 -0
  80. package/dist/picker/styles.d.ts.map +1 -0
  81. package/dist/picker/styles.js +116 -0
  82. package/dist/picker/styles.js.map +1 -0
  83. package/dist/picker/view.d.ts +135 -0
  84. package/dist/picker/view.d.ts.map +1 -0
  85. package/dist/picker/view.js +117 -0
  86. package/dist/picker/view.js.map +1 -0
  87. package/dist/picker/words.d.ts +53 -0
  88. package/dist/picker/words.d.ts.map +1 -0
  89. package/dist/picker/words.js +28 -0
  90. package/dist/picker/words.js.map +1 -0
  91. package/dist/reevaluate/render.d.ts +77 -0
  92. package/dist/reevaluate/render.d.ts.map +1 -0
  93. package/dist/reevaluate/render.js +211 -0
  94. package/dist/reevaluate/render.js.map +1 -0
  95. package/dist/reevaluate/styles.d.ts +24 -0
  96. package/dist/reevaluate/styles.d.ts.map +1 -0
  97. package/dist/reevaluate/styles.js +182 -0
  98. package/dist/reevaluate/styles.js.map +1 -0
  99. package/dist/reevaluate/view.d.ts +102 -0
  100. package/dist/reevaluate/view.d.ts.map +1 -0
  101. package/dist/reevaluate/view.js +111 -0
  102. package/dist/reevaluate/view.js.map +1 -0
  103. package/dist/reevaluate/words.d.ts +54 -0
  104. package/dist/reevaluate/words.d.ts.map +1 -0
  105. package/dist/reevaluate/words.js +29 -0
  106. package/dist/reevaluate/words.js.map +1 -0
  107. package/dist/scale/commands.d.ts +60 -0
  108. package/dist/scale/commands.d.ts.map +1 -0
  109. package/dist/scale/commands.js +49 -0
  110. package/dist/scale/commands.js.map +1 -0
  111. package/dist/scale/ladder.d.ts +141 -0
  112. package/dist/scale/ladder.d.ts.map +1 -0
  113. package/dist/scale/ladder.js +243 -0
  114. package/dist/scale/ladder.js.map +1 -0
  115. package/dist/scale/render.d.ts +85 -0
  116. package/dist/scale/render.d.ts.map +1 -0
  117. package/dist/scale/render.js +194 -0
  118. package/dist/scale/render.js.map +1 -0
  119. package/dist/scale/styles.d.ts +21 -0
  120. package/dist/scale/styles.d.ts.map +1 -0
  121. package/dist/scale/styles.js +105 -0
  122. package/dist/scale/styles.js.map +1 -0
  123. package/dist/workspace/inspector.d.ts +117 -0
  124. package/dist/workspace/inspector.d.ts.map +1 -0
  125. package/dist/workspace/inspector.js +140 -0
  126. package/dist/workspace/inspector.js.map +1 -0
  127. package/dist/workspace/rail.d.ts +122 -0
  128. package/dist/workspace/rail.d.ts.map +1 -0
  129. package/dist/workspace/rail.js +216 -0
  130. package/dist/workspace/rail.js.map +1 -0
  131. package/dist/workspace/render.d.ts +139 -0
  132. package/dist/workspace/render.d.ts.map +1 -0
  133. package/dist/workspace/render.js +366 -0
  134. package/dist/workspace/render.js.map +1 -0
  135. package/dist/workspace/selection.d.ts +92 -0
  136. package/dist/workspace/selection.d.ts.map +1 -0
  137. package/dist/workspace/selection.js +83 -0
  138. package/dist/workspace/selection.js.map +1 -0
  139. package/dist/workspace/styles.d.ts +39 -0
  140. package/dist/workspace/styles.d.ts.map +1 -0
  141. package/dist/workspace/styles.js +313 -0
  142. package/dist/workspace/styles.js.map +1 -0
  143. package/package.json +58 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The ladder chrome's structural stylesheet.
3
+ *
4
+ * Same contract as the viewer's, and for the same reason: it carries layout,
5
+ * weight and state, never a value. Every colour, length and font here is a
6
+ * `var(--ig-…)` the host's theme resolves, so a second theme keeps working on
7
+ * the surfaces layer 2 adds rather than only on layer 1's. `styles.test.ts`
8
+ * scans these bytes for a literal colour or a fixed length and for a token the
9
+ * theme does not define, so the rule is enforced rather than remembered.
10
+ *
11
+ * IT ADDS SELECTORS RATHER THAN REDEFINING THEM. The refusal, its capsules and
12
+ * their counts already have a look — this file styles only what layer 2
13
+ * introduces: the ladder container, the routes, the search box, the isolated
14
+ * chip and the list it opens.
15
+ *
16
+ * Shipped as a string for the same reason the viewer's is: an entry that
17
+ * imports CSS cannot be loaded by a bare Node runtime, and a string needs no
18
+ * bundler.
19
+ */
20
+ export const scaleLadderStylesheet = `
21
+ .ig-ladder {
22
+ display: flex;
23
+ flex-direction: column;
24
+ gap: var(--ig-space);
25
+ font-family: var(--ig-font-ui);
26
+ font-size: var(--ig-font-size);
27
+ line-height: var(--ig-line-height);
28
+ color: var(--ig-text-body);
29
+ }
30
+
31
+ .ig-ladder-routes {
32
+ list-style: none;
33
+ margin: 0;
34
+ padding: 0;
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: var(--ig-space-tight);
38
+ color: var(--ig-text-muted);
39
+ font-size: var(--ig-font-size-small);
40
+ }
41
+
42
+ .ig-ladder-search {
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: var(--ig-space-tight);
46
+ }
47
+
48
+ .ig-ladder-search input {
49
+ background: var(--ig-surface);
50
+ color: var(--ig-text);
51
+ border: var(--ig-stroke) solid var(--ig-line);
52
+ border-radius: var(--ig-radius);
53
+ padding: var(--ig-space-tight);
54
+ font-family: var(--ig-font-ui);
55
+ font-size: var(--ig-font-size);
56
+ }
57
+
58
+ .ig-ladder-search input:focus-visible {
59
+ outline: var(--ig-focus-ring) solid var(--ig-focus);
60
+ outline-offset: var(--ig-space-tight);
61
+ }
62
+
63
+ .ig-ladder-match {
64
+ display: flex;
65
+ gap: var(--ig-space-tight);
66
+ align-items: baseline;
67
+ }
68
+
69
+ .ig-chip {
70
+ align-self: flex-start;
71
+ background: var(--ig-surface-2);
72
+ color: var(--ig-text-body);
73
+ border: var(--ig-stroke) solid var(--ig-line);
74
+ border-radius: var(--ig-radius);
75
+ padding: var(--ig-space-tight) var(--ig-space);
76
+ font-family: var(--ig-font-ui);
77
+ font-size: var(--ig-font-size-small);
78
+ cursor: pointer;
79
+ }
80
+
81
+ .ig-chip:focus-visible {
82
+ outline: var(--ig-focus-ring) solid var(--ig-focus);
83
+ outline-offset: var(--ig-space-tight);
84
+ }
85
+
86
+ .ig-chip[aria-expanded='true'] {
87
+ border-color: var(--ig-accent);
88
+ color: var(--ig-text);
89
+ }
90
+
91
+ .ig-isolated-list {
92
+ list-style: none;
93
+ margin: 0;
94
+ padding: 0;
95
+ display: flex;
96
+ flex-direction: column;
97
+ gap: var(--ig-space-tight);
98
+ }
99
+
100
+ .ig-isolated-list .ig-id {
101
+ font-family: var(--ig-font-mono);
102
+ color: var(--ig-text-muted);
103
+ }
104
+ `;
105
+ //# sourceMappingURL=styles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.js","sourceRoot":"","sources":["../../src/scale/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoFpC,CAAC"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The inspector: detail, the "why", relationships, and the edit affordances
3
+ * that hang off them.
4
+ *
5
+ * ## It is a projection of the selection, and holds no state of its own
6
+ *
7
+ * §17b makes `selected` the one edge state that also filters the inspector, so
8
+ * this zone is a pure function of the document and that single value. It keeps
9
+ * no "currently inspecting" of its own — a second copy would be free to
10
+ * disagree with the rail and the canvas about what is selected, which is the
11
+ * whole reason the workspace owns exactly one selection.
12
+ *
13
+ * ## An edge selection FILTERS; it does not open a different panel
14
+ *
15
+ * The distinction matters and it is easy to lose. With an edge selected the
16
+ * inspector shows the same relationship list, narrowed to that one edge —
17
+ * rather than a separate edge inspector. So the reader's frame of reference
18
+ * never jumps.
19
+ *
20
+ * CLEARING RETURNS TO NOTHING SELECTED, NOT TO A WIDER LIST — and an earlier
21
+ * revision of this comment said the opposite, which is how a control came to be
22
+ * worded "show every relationship" while it emptied the panel. §17b makes the
23
+ * inspector a projection of the SELECTION, and `none` is a selection with no
24
+ * subject, so there is no list for it to widen to.
25
+ *
26
+ * The alternative was considered and not taken: `none` could list every
27
+ * relationship in the document, which would make the three states a clean
28
+ * narrowing — all, then one issue's, then one edge's — and make "show every
29
+ * relationship" literally true. It also invents a fourth thing for the panel to
30
+ * be, on a zone whose whole contract is that it shows what is selected. The
31
+ * cheaper repair was to stop the prose promising a behaviour the design does
32
+ * not have.
33
+ *
34
+ * ## Edge identity comes from `@issuegraph/core`
35
+ *
36
+ * A `ViewerEdge` carries no id — layer 1 has no need of one — but the picker,
37
+ * the overlays and the store all name edges by the identity `edgeIdentity`
38
+ * derives from content. Recomputing it here is composing the foundation both
39
+ * layers already sit on, which is the same crossing `picker/view.ts` declared
40
+ * and for the same reason: a local second spelling of an identity is how a host
41
+ * comes to hold two ids for one edge.
42
+ */
43
+ import type { EdgeField } from '@issuegraph/core';
44
+ import type { ViewerDocument, ViewerHold, ViewerIssue } from '@issuegraph/viewer';
45
+ import type { WorkspaceSelection } from './selection.ts';
46
+ /** One relationship, as the inspector lists it. */
47
+ export interface InspectorRelationship {
48
+ /** The identity `@issuegraph/core` derives — what a retype or delete names. */
49
+ readonly edgeId: string;
50
+ readonly field: EdgeField;
51
+ readonly from: string;
52
+ readonly to: string;
53
+ /**
54
+ * Which end the subject is on — `null` when there is no direction to state.
55
+ *
56
+ * A DIRECTION RELATIVE TO THE SUBJECT, not a sentence: "blocked-by, outgoing"
57
+ * is a fact, and the English for it is the host's to write. The package
58
+ * refuses to construct one, exactly as the picker does.
59
+ *
60
+ * NULL FOR A SYMMETRIC FIELD, and that is a fact about the FORMAT rather
61
+ * than a rendering choice. `serialize-with` and `together-with` state the
62
+ * same relationship whichever way round their endpoints are stored —
63
+ * `edgeIdentity` normalizes them for exactly that reason — so a direction
64
+ * read off the stored order is one that does not exist, and a host wording
65
+ * it would describe the relationship wrongly. `picker/view.ts` already
66
+ * refuses a direction for those two kinds; this now agrees with it instead
67
+ * of contradicting it from the next zone over.
68
+ *
69
+ * Also `null` when the subject is not an issue: an edge selection has no
70
+ * "my end" to be relative to.
71
+ */
72
+ readonly direction: 'outgoing' | 'incoming' | null;
73
+ }
74
+ /** What the inspector knows about the selected issue's position. */
75
+ export interface InspectorPosition {
76
+ /** 1-based, or `null` for a held slot — the viewer's own convention. */
77
+ readonly rank: number | null;
78
+ readonly ready: boolean;
79
+ readonly holds: readonly ViewerHold[];
80
+ }
81
+ export interface InspectorView {
82
+ /**
83
+ * What the selection resolved to.
84
+ *
85
+ * `none` covers both "nothing selected" and "the selection no longer
86
+ * resolves". They are deliberately one state rather than two: a selection
87
+ * that named a row a write has since removed is not an error a reader can
88
+ * act on, and rendering last frame's answer for it is the stale-detail bug
89
+ * that keeping no document on the selection exists to prevent.
90
+ */
91
+ readonly subject: {
92
+ readonly kind: 'none';
93
+ } | {
94
+ readonly kind: 'issue';
95
+ readonly issue: ViewerIssue;
96
+ readonly position: InspectorPosition | null;
97
+ } | {
98
+ readonly kind: 'edge';
99
+ readonly relationship: InspectorRelationship;
100
+ };
101
+ /**
102
+ * The relationships on show: every one touching the selected issue, the one
103
+ * selected edge, or none.
104
+ */
105
+ readonly relationships: readonly InspectorRelationship[];
106
+ /** Whether an edge selection is narrowing the list. Drives the clear control. */
107
+ readonly filtered: boolean;
108
+ }
109
+ /**
110
+ * Project a document and a selection onto the inspector.
111
+ *
112
+ * Pure and total: an unresolvable selection returns the empty view rather than
113
+ * throwing, because a document changing under a selection is ordinary — a write
114
+ * lands, the order recomputes — and is not a condition a reader can fix.
115
+ */
116
+ export declare function inspectorView(raw: ViewerDocument, selection: WorkspaceSelection): InspectorView;
117
+ //# sourceMappingURL=inspector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspector.d.ts","sourceRoot":"","sources":["../../src/workspace/inspector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,WAAW,EAAc,MAAM,oBAAoB,CAAC;AAG9F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEzD,mDAAmD;AACnD,MAAM,WAAW,qBAAqB;IACpC,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,SAAS,EAAE,UAAU,GAAG,UAAU,GAAG,IAAI,CAAC;CACpD;AAED,oEAAoE;AACpE,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;CACvC;AAED,MAAM,WAAW,aAAa;IAC5B;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,EACZ;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GACzB;QAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAA;KAAE,GACpG;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAA;KAAE,CAAC;IAC5E;;;OAGG;IACH,QAAQ,CAAC,aAAa,EAAE,SAAS,qBAAqB,EAAE,CAAC;IACzD,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAkCD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,cAAc,EACnB,SAAS,EAAE,kBAAkB,GAC5B,aAAa,CAmEf"}
@@ -0,0 +1,140 @@
1
+ /**
2
+ * The inspector: detail, the "why", relationships, and the edit affordances
3
+ * that hang off them.
4
+ *
5
+ * ## It is a projection of the selection, and holds no state of its own
6
+ *
7
+ * §17b makes `selected` the one edge state that also filters the inspector, so
8
+ * this zone is a pure function of the document and that single value. It keeps
9
+ * no "currently inspecting" of its own — a second copy would be free to
10
+ * disagree with the rail and the canvas about what is selected, which is the
11
+ * whole reason the workspace owns exactly one selection.
12
+ *
13
+ * ## An edge selection FILTERS; it does not open a different panel
14
+ *
15
+ * The distinction matters and it is easy to lose. With an edge selected the
16
+ * inspector shows the same relationship list, narrowed to that one edge —
17
+ * rather than a separate edge inspector. So the reader's frame of reference
18
+ * never jumps.
19
+ *
20
+ * CLEARING RETURNS TO NOTHING SELECTED, NOT TO A WIDER LIST — and an earlier
21
+ * revision of this comment said the opposite, which is how a control came to be
22
+ * worded "show every relationship" while it emptied the panel. §17b makes the
23
+ * inspector a projection of the SELECTION, and `none` is a selection with no
24
+ * subject, so there is no list for it to widen to.
25
+ *
26
+ * The alternative was considered and not taken: `none` could list every
27
+ * relationship in the document, which would make the three states a clean
28
+ * narrowing — all, then one issue's, then one edge's — and make "show every
29
+ * relationship" literally true. It also invents a fourth thing for the panel to
30
+ * be, on a zone whose whole contract is that it shows what is selected. The
31
+ * cheaper repair was to stop the prose promising a behaviour the design does
32
+ * not have.
33
+ *
34
+ * ## Edge identity comes from `@issuegraph/core`
35
+ *
36
+ * A `ViewerEdge` carries no id — layer 1 has no need of one — but the picker,
37
+ * the overlays and the store all name edges by the identity `edgeIdentity`
38
+ * derives from content. Recomputing it here is composing the foundation both
39
+ * layers already sit on, which is the same crossing `picker/view.ts` declared
40
+ * and for the same reason: a local second spelling of an identity is how a host
41
+ * comes to hold two ids for one edge.
42
+ */
43
+ import { edgeIdentity, isSymmetricEdgeField } from '@issuegraph/core';
44
+ import { normalizeDocument } from '@issuegraph/viewer';
45
+ const EMPTY = {
46
+ subject: { kind: 'none' },
47
+ relationships: [],
48
+ filtered: false,
49
+ };
50
+ /** Every relationship in the document, carrying the identity edits name. */
51
+ function relationshipsOf(document, subject) {
52
+ return document.edges.map((edge) => ({
53
+ edgeId: edgeIdentity(edge.field, edge.from, edge.to),
54
+ field: edge.field,
55
+ from: edge.from,
56
+ to: edge.to,
57
+ direction: subject === null ||
58
+ isSymmetricEdgeField(edge.field) ||
59
+ (edge.from !== subject && edge.to !== subject)
60
+ ? null
61
+ : edge.from === subject
62
+ ? 'outgoing'
63
+ : 'incoming',
64
+ }));
65
+ }
66
+ /** The slot carrying a key, if the order places it. */
67
+ function slotFor(document, key) {
68
+ return document.order.slots.find((slot) => slot.members.includes(key));
69
+ }
70
+ /**
71
+ * Project a document and a selection onto the inspector.
72
+ *
73
+ * Pure and total: an unresolvable selection returns the empty view rather than
74
+ * throwing, because a document changing under a selection is ordinary — a write
75
+ * lands, the order recomputes — and is not a condition a reader can fix.
76
+ */
77
+ export function inspectorView(raw, selection) {
78
+ if (selection.kind === 'none')
79
+ return EMPTY;
80
+ // IT NORMALIZES ITS OWN INPUT, so the list can never disagree with what the
81
+ // other zones drew. Reading the raw edges published relationships layer 1 had
82
+ // already dropped — a dangling edge, a repeated one, a self-edge — each with
83
+ // a live `select-edge` command on it, so the inspector offered the reader an
84
+ // edge that exists on no other surface.
85
+ //
86
+ // Inside rather than at the call site, because this is exported: a host
87
+ // calling it directly gets the same answer the workspace does. Idempotent, so
88
+ // the workspace normalizing first costs a pass and changes nothing.
89
+ const document = normalizeDocument(raw).document;
90
+ if (selection.kind === 'issue') {
91
+ // CANONICALIZED TO THE SLOT LEAD, because a together unit is ONE row and
92
+ // layer 1 has already decided which member speaks for it. `ViewerSlot.lead`
93
+ // is documented as "the detail surface's subject", and both projections run
94
+ // their options through `atStations` before drawing — so a selection naming
95
+ // a PARTNER marked the lead current in the rail and on the canvas while this
96
+ // panel showed the partner's title and relationships. Two zones naming
97
+ // different issues for one selection, which is the exact thing holding a
98
+ // single selection value was supposed to make impossible.
99
+ //
100
+ // Falls back to the key itself when the order does not place it: an
101
+ // excluded or unplaced issue is its own subject, and there is no lead to
102
+ // defer to.
103
+ const placement = slotFor(document, selection.key);
104
+ const subject = placement?.lead ?? selection.key;
105
+ const issue = document.issues.find((candidate) => candidate.key === subject);
106
+ if (issue === undefined)
107
+ return EMPTY;
108
+ const slot = placement;
109
+ return {
110
+ subject: {
111
+ kind: 'issue',
112
+ issue,
113
+ // NULL RATHER THAN A FABRICATED POSITION. An issue the order excludes —
114
+ // a duplicate — genuinely has no position, and inventing `rank: null,
115
+ // ready: false` for it would read as a hold, which is a different fact
116
+ // with a different remedy.
117
+ position: slot === undefined
118
+ ? null
119
+ : { rank: slot.rank, ready: slot.ready, holds: slot.holds },
120
+ },
121
+ // FILTERED ON MEMBERSHIP, NOT ON DIRECTION. Those were the same question
122
+ // only while every edge touching the subject had one — and the moment a
123
+ // symmetric field correctly reported `null`, a `together-with` or
124
+ // `serialize-with` on the selected issue disappeared from the panel
125
+ // entirely. "Does this edge touch my subject" and "which end is my
126
+ // subject on" are different questions, and only the first belongs here.
127
+ relationships: relationshipsOf(document, subject).filter((relationship) => relationship.from === subject || relationship.to === subject),
128
+ filtered: false,
129
+ };
130
+ }
131
+ const relationship = relationshipsOf(document, null).find((candidate) => candidate.edgeId === selection.edgeId);
132
+ if (relationship === undefined)
133
+ return EMPTY;
134
+ return {
135
+ subject: { kind: 'edge', relationship },
136
+ relationships: [relationship],
137
+ filtered: true,
138
+ };
139
+ }
140
+ //# sourceMappingURL=inspector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspector.js","sourceRoot":"","sources":["../../src/workspace/inspector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The order rail: always present, always complete, and virtualised.
3
+ *
4
+ * ## The asymmetry with the canvas is the design, not an inconsistency
5
+ *
6
+ * §17f gives the two centre zones opposite obligations. The canvas answers
7
+ * "what surrounds this issue" and REFUSES above its budget — that is #10343's
8
+ * scale ladder, and it is correct. The rail answers "what gets worked next" for
9
+ * the whole backlog and must NEVER refuse. Assembling them must not average the
10
+ * two: a rail that paginates has stopped answering its question, and a canvas
11
+ * that tries to keep up with the rail has stopped honouring its budget.
12
+ *
13
+ * So virtualisation here is a requirement rather than an optimisation, and it
14
+ * is the resolution of what reads at first like a contradiction — "complete"
15
+ * and "windowed" at once.
16
+ *
17
+ * **The MODEL is complete; the WINDOW is bounded.** Every slot the host handed
18
+ * over is held, and {@link RailWindow.addressOf} answers for every rank in the
19
+ * order whether or not it is drawn. What is bounded is only how many rows are
20
+ * rendered at a time. Nothing is dropped, nothing is refused, and a host that
21
+ * scrolls asks for a different window over the same complete model.
22
+ *
23
+ * That is what "every rank is addressable" means, and it is the property the
24
+ * acceptance suite drives at 312 rows.
25
+ *
26
+ * ## `addressOf` is a closure over a private index
27
+ *
28
+ * The same shape — and the same reasoning — as `AuditOverlay.rowFor`. Handing
29
+ * out the index itself would let a JavaScript consumer mutate it, after which
30
+ * the lookups and `total` would disagree about what the order contains, which
31
+ * is the inconsistency this value exists to prevent. A closure cannot be
32
+ * reached at all, and still answers in constant time.
33
+ *
34
+ * ## What gets windowed
35
+ *
36
+ * Slots are sliced, and the issues and edges are cut down to exactly what the
37
+ * drawn rows need — a drawn slot's members, an exclusion's key, and the
38
+ * endpoints of any edge a drawn row owes a badge for.
39
+ *
40
+ * KEEPING THE WHOLE ISSUE LIST WAS THE OBVIOUS THING AND IT WAS WRONG. See the
41
+ * note at the filter itself for what it cost: the linear projection renders a
42
+ * count of the keys that appear in no slot and on no edge, so every edgeless
43
+ * issue outside the window was reported to the reader as isolated — the rail
44
+ * describing the reader's scroll position as though it were the document.
45
+ *
46
+ * `together-with` keeps its own rule, and it is layer 1's rather than a choice
47
+ * made here: that edge draws as an ENCLOSURE around one slot's members, so the
48
+ * viewer drops — and reports — any whose members no longer share a drawn slot.
49
+ * Those edges are left out of the windowed document and counted on
50
+ * {@link RailWindow.undrawn} instead, so the fact is reported as a property of
51
+ * the WINDOW rather than leaking out as a document defect.
52
+ */
53
+ import type { ViewerDocument, ViewerSlot } from '@issuegraph/viewer';
54
+ /**
55
+ * How many rows a window holds when the host does not say.
56
+ *
57
+ * A round number rather than a measured one: this package has no mount and
58
+ * therefore no viewport to measure, so any default here is a guess. It is
59
+ * published so a host can see what it is overriding, and generous enough that
60
+ * a first paint fills an ordinary screen.
61
+ */
62
+ export declare const RAIL_WINDOW = 50;
63
+ export interface RailWindowOptions {
64
+ /**
65
+ * The first row to draw, as a 0-BASED offset into the order's slots.
66
+ *
67
+ * An offset, deliberately, and not a rank: a held slot has `rank === null`,
68
+ * so ranks are not a coordinate you can slice on. Out-of-range values are
69
+ * clamped rather than refused — a host that scrolls past the end gets the
70
+ * last window, which is what a scroll container does anyway.
71
+ */
72
+ readonly start?: number | undefined;
73
+ /** How many rows to draw. Defaults to {@link RAIL_WINDOW}. */
74
+ readonly count?: number | undefined;
75
+ }
76
+ export interface RailWindow {
77
+ /** Every slot in the order, drawn or not. The completeness claim, as a number. */
78
+ readonly total: number;
79
+ /** The 0-based offset of the first drawn row, after clamping. */
80
+ readonly start: number;
81
+ /** How many rows are drawn. Fewer than asked for at the end of the order. */
82
+ readonly count: number;
83
+ /** Slots before the window. A host sizes its leading spacer from this. */
84
+ readonly before: number;
85
+ /** Slots after the window. A host sizes its trailing spacer from this. */
86
+ readonly after: number;
87
+ /** The drawn slots, in order. */
88
+ readonly rows: readonly ViewerSlot[];
89
+ /** The document to hand the viewer: the windowed slots, and what they need. */
90
+ readonly document: ViewerDocument;
91
+ /**
92
+ * `together-with` edges left out because their unit is outside the window.
93
+ *
94
+ * Reported rather than silent: it is the one thing windowing costs, and a
95
+ * host drawing a connector count of its own needs to know the window is not
96
+ * the whole story.
97
+ */
98
+ readonly undrawn: number;
99
+ /**
100
+ * The slot at a rank — ANY rank in the order, drawn or not.
101
+ *
102
+ * This is the completeness property, and it is why the rail can be windowed
103
+ * without ceasing to answer its question: a reader asking "what is at 287?"
104
+ * gets an answer from a rail showing rows 1–50.
105
+ *
106
+ * Ranks are 1-BASED, matching what the rail renders and what a reader sees.
107
+ * A held slot has no rank at all and is unreachable this way by construction
108
+ * — it has no position in the sequence, which is precisely what `rank: null`
109
+ * says.
110
+ */
111
+ readonly addressOf: (rank: number) => ViewerSlot | undefined;
112
+ /** The 0-based offset of a slot's lead key, for a host scrolling to it. */
113
+ readonly offsetOf: (key: string) => number | undefined;
114
+ }
115
+ /**
116
+ * Window an order without narrowing it.
117
+ *
118
+ * Pure, and total over any document: there is no input that makes this throw or
119
+ * refuse, because the rail is the zone that is not allowed to.
120
+ */
121
+ export declare function railWindow(raw: ViewerDocument, options?: RailWindowOptions): RailWindow;
122
+ //# sourceMappingURL=rail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rail.d.ts","sourceRoot":"","sources":["../../src/workspace/rail.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGrE;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,KAAK,CAAC;AAE9B,MAAM,WAAW,iBAAiB;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,UAAU;IACzB,kFAAkF;IAClF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iCAAiC;IACjC,QAAQ,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,CAAC;IACrC,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAC;IAC7D,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;CACxD;AA4BD;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,cAAc,EACnB,OAAO,GAAE,iBAAsB,GAC9B,UAAU,CAgIZ"}