spyret 0.2.0 → 0.3.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.
@@ -8,7 +8,10 @@ integration is a separate `spyret/browser` entry.
8
8
  ## Import a published version in CPO
9
9
 
10
10
  A maintainer runs `npm run release:drive` and follows the
11
- [manual upload instructions](RELEASING.md). Use the import line they supply:
11
+ [CPO-save and Drive publishing instructions](RELEASING.md). The native JavaScript
12
+ requires CPO per-file authorization; public sharing alone is insufficient.
13
+ Verify the import with a second account using normal login before distributing it.
14
+ Use the import line the maintainer supplies:
12
15
 
13
16
  ```pyret
14
17
  import shared-gdrive("spyret-vVERSION.arr", "WRAPPER_DRIVE_FILE_ID") as S
@@ -22,9 +25,11 @@ only one import for typed constructors and diagram functions.
22
25
 
23
26
  ## An ordinary native import in CPO
24
27
 
25
- CPO already supports native Pyret modules through `gdrive-js`. Upload the built
26
- `dist/spyret.pyret.js` to Google Drive **as `spyret.js`**, and grant intended users
27
- read access. Do not convert it to a Google document. With that file's ID:
28
+ CPO supports native Pyret modules through `gdrive-js`. Save the complete contents
29
+ of `dist/spyret.pyret.js` through a new CPO editor document **as `spyret.js`**,
30
+ without running it, following [the release workflow](RELEASING.md). Grant intended
31
+ users read access in Drive; their CPO per-file authorization must also be checked.
32
+ Do not convert the file to a Google document. With the saved file's ID:
28
33
 
29
34
  ```pyret
30
35
  import gdrive-js("spyret.js", "YOUR_DRIVE_FILE_ID") as Spyret
package/docs/RELEASING.md CHANGED
@@ -9,18 +9,57 @@ npm run release:drive
9
9
 
10
10
  The script downloads the latest GitHub release and walks you through:
11
11
 
12
- 1. Upload the prepared `.js` file to your Google Drive. Set **Anyone with the
13
- link / Viewer**, then paste its Drive link into the script.
14
- 2. Upload the generated `.arr` file with the same sharing setting and paste
15
- its Drive link into the script.
12
+ 1. Open a new [CPO editor](https://code.pyret.org/editor) with normal Google login.
13
+ Replace **all** editor text (including the initial `use context` line) with the
14
+ contents of the prepared `.js` file. Name it `spyret-vVERSION.js`, **Save**, and
15
+ wait for saving to finish. **Do not Run** this document: it contains JavaScript.
16
+ In Google Drive, set the saved file to **Anyone with the link / Viewer** and
17
+ keep it under **My Drive**. Paste its CPO editor URL (`#program=...`), Drive
18
+ link, or file ID into the script. Use the saved file, not a CPO Share copy.
19
+ 2. Upload the generated `.arr` wrapper to My Drive with the same sharing setting
20
+ and paste its Drive link into the script.
16
21
  3. Copy the printed import into CPO and run the provided diagram example.
17
- Share that import line with users: it includes typed constructors and diagrams.
22
+ 4. Test that same import with a **second account using normal CPO login** before
23
+ distributing it. The script prepares artifacts; it does not verify Google
24
+ permissions or perform either browser test.
18
25
 
19
- Keep both filenames unchanged and upload them as files without conversion.
26
+ ### Why save the JavaScript through CPO?
27
+
28
+ CPO's normal login requests `drive.file`. Its `gdrive-js` loader uses an
29
+ authenticated request, so uploading a public JavaScript file directly to Drive
30
+ does not by itself authorize CPO to read it. Saving through CPO creates the file
31
+ with CPO's authorization. CPO saves the editor text without compiling it.
32
+ The outer `.arr` wrapper uses `shared-gdrive`, which has a public-file retrieval
33
+ path, so that file can still be uploaded directly.
34
+
35
+ For an existing JavaScript upload, **Open with → Code Pyret** in Google Drive,
36
+ if offered, is another way to grant per-file access without changing its ID.
37
+ Google documents this mechanism in its
38
+ [Drive authorization guidance](https://developers.google.com/workspace/drive/api/guides/handle-errors#appNotAuthorizedToFile).
39
+ Opening an arbitrary `editor#program=ID` link alone is not the same authorization
40
+ action. Pasting a link into this script only selects an ID; it grants no access.
41
+
42
+ This workflow still needs end-to-end verification with real CPO accounts.
43
+ Success as the publisher does not establish access for other users. If another
44
+ user gets a 403, they may need to authorize the JavaScript through Drive's
45
+ **Open with → Code Pyret** too. Do not advertise a setup-free import until the
46
+ second-account test passes. The old full-access login flow is not a prerequisite
47
+ or a recommended workaround: Google may block it.
48
+
49
+ Keep both filenames unchanged and preserve the file contents without conversion.
50
+ Use **My Drive**, not a Google **Shared drive**: CPO’s Drive loader omits the
51
+ shared-drive API flag, so files in Shared drives return 404 even when public.
52
+ A shortcut under My Drive does not change where a file lives.
20
53
  The two upload files are in `release/vVERSION/upload/`; the CPO example is
21
54
  saved separately as `release/vVERSION/import.arr`. Keep published Drive files
22
55
  unchanged so existing imports continue to work.
23
56
 
57
+ If you previously uploaded directly to Drive (or used a Shared drive), rerun with
58
+ `npm run release:drive -- --tag v0.2.0 --out release/cpo-authorized` and follow
59
+ the CPO-save workflow above. Paste the new links when prompted so the wrapper
60
+ uses the new JavaScript file ID. The fresh output directory preserves the old
61
+ release artifacts; existing wrappers are never overwritten with a different ID.
62
+
24
63
  To select a specific release, use `npm run release:drive -- --tag v0.2.0`.
25
64
  The release must contain the browser module and Pyret rules; `v0.1.1` is headless
26
65
  and cannot be used. No Google Cloud project, service account, or API setup is needed.
@@ -13,8 +13,8 @@ data Tree:
13
13
  sharing:
14
14
  method _spytial(self) -> List<S.SpytialRule>:
15
15
  [list:
16
- S.orientation("children", [list: S.direction-below]),
17
- S.align("siblings", S.alignment-horizontal)
16
+ S.orientation("children", [list: S.below]),
17
+ S.align("siblings", S.horizontal)
18
18
  ]
19
19
  end
20
20
  end
@@ -22,13 +22,11 @@ end
22
22
 
23
23
  Public constructors wrap their results as `constraint(Constraint)` or
24
24
  `directive(Directive)` automatically. Compose rules with ordinary Pyret lists;
25
- the serializer puts each rule in its proper YAML section. Optional fields and
26
- style blocks have typed, immutable setters:
25
+ the serializer puts each rule in its proper YAML section. Use named fields in
26
+ records for optional properties and typed constructors for nested style blocks:
27
27
 
28
28
  ```pyret
29
- S.atom-style-with(S.default-atom-style-options
30
- .with-selector("leaf")
31
- .with-fill-style(S.default-fill-style.with-color("red")))
29
+ S.atom-style("leaf", {fill-style: S.fill-style({color: "red"})})
32
30
  ```
33
31
 
34
32
  From the JavaScript host, after evaluating the program:
@@ -60,8 +58,9 @@ rule list directly. Capture and `toDataInstance` never invoke hooks, and omit
60
58
  callable `_spytial` metadata on ordinary objects or outside a datatype's declared
61
59
  slots. Other capture restrictions still apply.
62
60
 
63
- See the [generated rule reference](SPYTIAL_RULES_REFERENCE.md) for every
64
- constructor, enum and option. Pyret annotations check field types; serialization
61
+ See the [layout rule guide](SPYTIAL_LANGUAGE.md) for examples and the
62
+ [generated rule reference](SPYTIAL_RULES_REFERENCE.md) for every constructor,
63
+ enum and option. Pyret annotations check field types; serialization
65
64
  checks numeric bounds, string patterns and incompatible directions. Selector
66
65
  meaning and result arity remain Core's responsibility.
67
66
 
@@ -70,4 +69,3 @@ then run `npm run generate:spytial`. Commit the generated Pyret source, serializ
70
69
  schema and reference together. `npm run check:spytial` and the tests detect drift;
71
70
  unknown field types or enum vocabularies fail generation for explicit handling.
72
71
  No Core runtime dependency is added to the published package.
73
-
@@ -0,0 +1,194 @@
1
+ # Spyret's layout rule language
2
+
3
+ Spyret lets a Pyret program describe how its data should be drawn. The
4
+ `pyret/spytial.arr` library, imported as `S` below, provides typed constructors
5
+ for Spytial constraints and directives. Each constructor returns an
6
+ `S.SpytialRule`; put rules in an ordinary Pyret list. `S.diagram(value)` finds
7
+ `_spytial` hooks on reachable values, collects their rules, and displays the
8
+ diagram. See [layout hooks](SPYTIAL_HOOKS.md) for the hook contract and
9
+ [the README](../README.md#pyret-import-describe-display) for the CPO import.
10
+ This page describes the 0.3.0 source API. The published v0.2.0 wrapper uses
11
+ prefixed enum names and `atom-style-with`; update those calls when moving to
12
+ 0.3.0. Style properties now use named records and typed blocks instead of
13
+ `.with-*` chains.
14
+
15
+ ## A first diagram
16
+
17
+ ```pyret
18
+ # Use the versioned wrapper import supplied by the library maintainer.
19
+ import shared-gdrive("spyret-vVERSION.arr", "WRAPPER_DRIVE_FILE_ID") as S
20
+
21
+ data Tree:
22
+ | leaf(value)
23
+ | branch(left, right)
24
+ sharing:
25
+ method _spytial(self) -> List<S.SpytialRule>:
26
+ [list:
27
+ S.orientation("left + right", [list: S.below]),
28
+ S.orientation("left", [list: S.left]),
29
+ S.orientation("right", [list: S.right]),
30
+ S.atom-style("leaf", {fill-style: S.fill-style({color: "#e0f2ff"})})
31
+ ]
32
+ end
33
+ end
34
+
35
+ S.diagram(branch(leaf(1), leaf(2)))
36
+ ```
37
+
38
+ The `left + right` selector finds parent-to-child pairs. For each pair,
39
+ `below` places the child below the parent. The next two rules put
40
+ left children to the left and right children to the right. The style rule fills
41
+ leaf nodes. Rule order does not turn later rules into overrides; Spytial applies
42
+ them together and reports conflicting constraints.
43
+
44
+ The same constructors work in a local `import file("spytial.arr") as S` if the
45
+ library is available as a file. That import supplies rule constructors; the
46
+ versioned wrapper also supplies `diagram` and `diagram-with-rules`.
47
+
48
+ ## Selectors and direction
49
+
50
+ A selector is a **string containing a Spytial relational expression**, not a
51
+ Pyret function. A unary selector such as `"leaf"` selects nodes. A binary
52
+ selector such as `"left"` or `"left + right"` selects `(source, target)` pairs.
53
+ The expected arity depends on the rule. For orientation, the direction names
54
+ describe where the **target** goes relative to the source. Thus, if `left`
55
+ contains `(parent, child)`, `S.below` puts the child below its parent.
56
+
57
+ Common expressions are a type name (`"leaf"`), a field name (`"left"`), a
58
+ union (`"left + right"`), or a transitive closure (`"^(left + right)"`). Keep
59
+ operators inside the quoted Pyret string. See [Spytial Core's selector
60
+ documentation](https://github.com/sidprasad/spytial-core/blob/main/site/selectors.md)
61
+ for the full expression language and arity rules.
62
+
63
+ | Pyret direction | Meaning for the target |
64
+ | --- | --- |
65
+ | `S.above`, `S.below` | Strictly above or below; sideways offset is allowed. |
66
+ | `S.left`, `S.right` | Strictly left or right; vertical offset is allowed. |
67
+ | `S.directly-above`, `S.directly-below` | Above or below and horizontally centered. |
68
+ | `S.directly-left`, `S.directly-right` | Left or right and vertically centered. |
69
+
70
+ You can combine compatible directions, for example
71
+ `[list: S.below, S.left]`. Opposites such as `above` and
72
+ `below` cannot be combined. A `directly-*` direction can only be combined with
73
+ its matching plain direction. Spyret checks these combinations when it
74
+ serializes the rule list.
75
+
76
+ ## Constraints: layout and visibility
77
+
78
+ | Constructor | What it does |
79
+ | --- | --- |
80
+ | `S.orientation(selector, directions)` | Places targets of a binary selector relative to sources. |
81
+ | `S.cyclic(selector)` | Arranges nodes in the order of a binary selector around a circle. |
82
+ | `S.align(selector, direction)` | Gives selected nodes or node pairs a common row (`S.horizontal`) or column (`S.vertical`). |
83
+ | `S.group(selector, name)` | Draws a box around selected nodes; a binary selector makes a group per first-column key. |
84
+ | `S.size(width, height)` | Sets node dimensions in pixels; select the affected nodes with `S.size-with(width, height, {selector: "..."})`. Both numbers must be positive. |
85
+ | `S.hide-atom(selector)` | Removes selected nodes and their edges from the diagram. |
86
+
87
+ For example:
88
+
89
+ ```pyret
90
+ [list:
91
+ S.cyclic-with("next", {direction: S.counterclockwise}),
92
+ S.align("siblings", S.horizontal),
93
+ S.group("children", "Family"),
94
+ S.size-with(150, 80, {selector: "branch"}),
95
+ S.hide-atom("InternalNode")
96
+ ]
97
+ ```
98
+
99
+ Constraints can also use `{hold: S.never}` to require that a relation
100
+ **does not** hold. For instance, negating `S.above` means the target is
101
+ not strictly above the source; it does not require the target to be below.
102
+ `S.always` expresses the normal positive constraint.
103
+
104
+ ## Directives: appearance and labels
105
+
106
+ | Constructor | What it does |
107
+ | --- | --- |
108
+ | `S.flag(name)` | Applies a global display flag such as `S.hide-disconnected` or `S.hide-disconnected-built-ins`. |
109
+ | `S.atom-style(selector, style)` | Styles the selected nodes' fill, border, icon, label, and label visibility. |
110
+ | `S.edge-style(field, style)` | Styles a relation's edge line and label, or hides the edge. |
111
+ | `S.attribute(field)` | Shows a field as text inside its source node instead of as an edge. |
112
+ | `S.tag(to-tag, name, value)` | Adds computed text to matching nodes while retaining the original edges. |
113
+ | `S.hide-field(field)` | Hides a field's drawn edges. |
114
+ | `S.inferred-edge(name, selector)` | Draws additional edges computed from a selector. |
115
+
116
+ `field` names a relation; optional `selector` and `filter` fields narrow where
117
+ field-based directives apply. `tag` takes a unary `to-tag` selector and a
118
+ `value` selector whose first column identifies the node receiving the tag.
119
+
120
+ ```pyret
121
+ [list:
122
+ S.flag(S.hide-disconnected-built-ins),
123
+ S.atom-style("leaf", {border-style: S.border-style({
124
+ color: "#2563eb", width: 2
125
+ })}),
126
+ S.edge-style("left", {line-style: S.line-style({
127
+ color: "#64748b", pattern: S.dashed
128
+ })}),
129
+ S.attribute("value"),
130
+ S.hide-field("internal"),
131
+ S.inferred-edge("descendant", "^(left + right)")
132
+ ]
133
+ ```
134
+
135
+ Use `atom-style`, `edge-style`, and `inferred-edge` for new styles. The exported
136
+ `icon`, `atom-color`, and `edge-color` constructors are older forms retained for
137
+ compatibility.
138
+
139
+ ## Optional fields and reusable style blocks
140
+
141
+ Rules with optional fields have a short constructor for their required
142
+ arguments and a `-with` constructor that takes a Pyret record of named fields.
143
+ `atom-style` takes a selector and a style record; `edge-style` takes a field
144
+ and a style record. Within those records, nested styles use the named types
145
+ `S.TextStyle`, `S.LineStyle`, `S.FillStyle`, `S.BorderStyle`, and `S.IconStyle`.
146
+ For example, `S.text-style({color: "navy"})` makes a `TextStyle`. Leave any
147
+ field out to let Spytial Core supply its display default.
148
+
149
+ ```pyret
150
+ S.orientation-with("children", [list: S.below], {hold: S.always})
151
+
152
+ S.group-with("children", "Family", {
153
+ add-edge: S.group-add-edge({
154
+ points: S.togroup,
155
+ line-style: S.line-style({weight: 2})
156
+ }),
157
+ text-style: S.text-style({color: "#7c3aed"})
158
+ })
159
+ ```
160
+
161
+ The group example draws a connector from each group key to its box. Its
162
+ `line-style` styles that connector; the group's own `text-style` styles its
163
+ caption. Most rule records also accept a `source: S.rule-source({...})` field;
164
+ this records the originating rule text and optional location
165
+ for error reports without changing layout.
166
+
167
+ The [generated rule reference](SPYTIAL_RULES_REFERENCE.md) lists every
168
+ constructor, option, enum, and style block for the currently pinned Core
169
+ language version. Spyret checks Pyret types through annotations, then checks
170
+ numeric bounds, string patterns, and direction compatibility while serializing.
171
+
172
+ ## How rules become a diagram
173
+
174
+ `_spytial` returns `List<S.SpytialRule>`. Spyret turns each list into one YAML
175
+ document with `constraints` and `directives` sections; constructors choose the
176
+ correct section automatically. `S.diagram(value)` collects the hooks reachable
177
+ from `value`. To bypass hooks, call `S.diagram-with-rules(value, rules)`; an
178
+ existing YAML document can be passed as `S.diagram(value, yaml)`.
179
+
180
+ For example, `S.orientation("left", [list: S.below])` represents the
181
+ following constraint (the serializer quotes YAML keys and strings):
182
+
183
+ ```yaml
184
+ constraints:
185
+ - orientation:
186
+ selector: left
187
+ directions: [below]
188
+ directives: []
189
+ ```
190
+
191
+ For host code, `getSpytialSpec(value, runtime)` returns the collected YAML
192
+ documents as `string[]`, and `spytialRulesToYaml(rules, runtime)` serializes an
193
+ already evaluated list. See [layout hooks](SPYTIAL_HOOKS.md) for collection
194
+ order, error behavior, and runtime requirements.
@@ -3,9 +3,11 @@
3
3
  Core 6.3.2; language 2026-09-18.
4
4
 
5
5
  Import `pyret/spytial.arr` as `S`. Constructors return `S.SpytialRule` automatically.
6
- Optional fields use typed options. Start with `S.default-<rule>-options`,
7
- chain `.with-<field>(value)`, then pass it to `<rule>-with` after the required arguments.
8
- Style blocks similarly offer `default-<block>` and `.with-<field>(value)`.
6
+ Enum values are qualified through `S`, for example `S.below` and `S.horizontal`.
7
+ Optional fields use Pyret records with named keys. Pass the record to `<rule>-with`;
8
+ `atom-style(selector, style)` and `edge-style(field, style)` take one directly.
9
+ Nested style blocks are typed constructors such as `S.fill-style({color: "red"})`.
10
+ Unset fields are omitted from YAML; Spytial Core supplies their defaults.
9
11
 
10
12
  Numeric bounds, patterns and incompatible direction combinations are checked during serialization.
11
13
 
@@ -18,8 +20,8 @@ Numeric bounds, patterns and incompatible direction combinations are checked dur
18
20
  | `size(width :: Number, height :: Number)` | `selector: String`, `source: RuleSource` |
19
21
  | `hide-atom(selector :: String)` | `source: RuleSource` |
20
22
  | `flag(name :: LayoutFlag)` | |
21
- | `atom-style()` | `selector: String`, `fill-style: FillStyle`, `border-style: BorderStyle`, `icon-style: IconStyle`, `text-style: TextStyle`, `show-label: Boolean`, `source: RuleSource` |
22
- | `edge-style(field :: String)` | `selector: String`, `filter: String`, `line-style: LineStyle`, `text-style: TextStyle`, `show-label: Boolean`, `hidden: Boolean`, `source: RuleSource` |
23
+ | `atom-style(selector :: String, style :: Any)` | A record with optional `fill-style`, `border-style`, `icon-style`, `text-style`, `show-label`, and `source` keys. |
24
+ | `edge-style(field :: String, style :: Any)` | A record with optional `selector`, `filter`, `line-style`, `text-style`, `show-label`, `hidden`, and `source` keys. |
23
25
  | `attribute(field :: String)` | `selector: String`, `filter: String`, `text-style: TextStyle`, `source: RuleSource` |
24
26
  | `tag(to-tag :: String, name :: String, value :: String)` | `text-style: TextStyle`, `source: RuleSource` |
25
27
  | `hide-field(field :: String)` | `selector: String`, `filter: String`, `source: RuleSource` |
@@ -30,22 +32,22 @@ Numeric bounds, patterns and incompatible direction combinations are checked dur
30
32
 
31
33
  ## Enum values
32
34
 
33
- - `TextSize`: `text-size-small`, `text-size-normal`, `text-size-large`
34
- - `LinePattern`: `line-pattern-solid`, `line-pattern-dashed`, `line-pattern-dotted`
35
- - `IconPlacement`: `icon-placement-full`, `icon-placement-badge`
36
- - `Direction`: `direction-above`, `direction-below`, `direction-left`, `direction-right`, `direction-directly-above`, `direction-directly-below`, `direction-directly-left`, `direction-directly-right`
37
- - `Hold`: `hold-always`, `hold-never`
38
- - `Rotation`: `rotation-clockwise`, `rotation-counterclockwise`
39
- - `Alignment`: `alignment-horizontal`, `alignment-vertical`
40
- - `GroupEdgeDirection`: `group-edge-direction-none`, `group-edge-direction-togroup`, `group-edge-direction-fromgroup`
41
- - `LayoutFlag`: `layout-flag-hide-disconnected`, `layout-flag-hide-disconnected-built-ins`
35
+ - `TextSize`: `small`, `normal`, `large`
36
+ - `LinePattern`: `solid`, `dashed`, `dotted`
37
+ - `IconPlacement`: `full`, `badge`
38
+ - `Direction`: `above`, `below`, `left`, `right`, `directly-above`, `directly-below`, `directly-left`, `directly-right`
39
+ - `Hold`: `always`, `never`
40
+ - `Rotation`: `clockwise`, `counterclockwise`
41
+ - `Alignment`: `horizontal`, `vertical`
42
+ - `GroupEdgeDirection`: `no-group-edge`, `togroup`, `fromgroup`
43
+ - `LayoutFlag`: `hide-disconnected`, `hide-disconnected-built-ins`
42
44
 
43
45
  ## Blocks
44
46
 
45
- - `text-style(size: Option<TextSize>, color: Option<String>)`
46
- - `line-style(color: Option<String>, pattern: Option<LinePattern>, weight: Option<Number>, highlight: Option<String>)`
47
- - `fill-style(color: Option<String>)`
48
- - `border-style(color: Option<String>, width: Option<Number>)`
49
- - `icon-style(path: Option<String>, placement: Option<IconPlacement>, opacity: Option<Number>)`
50
- - `rule-source(text: String, location: Option<String>)`
51
- - `group-add-edge(points: Option<GroupEdgeDirection>, line-style: Option<LineStyle>, text-style: Option<TextStyle>)`
47
+ - `text-style({size: TextSize?, color: String?})`
48
+ - `line-style({color: String?, pattern: LinePattern?, weight: Number?, highlight: String?})`
49
+ - `fill-style({color: String?})`
50
+ - `border-style({color: String?, width: Number?})`
51
+ - `icon-style({path: String?, placement: IconPlacement?, opacity: Number?})`
52
+ - `rule-source({text: String, location: String?})`
53
+ - `group-add-edge({points: GroupEdgeDirection?, line-style: LineStyle?, text-style: TextStyle?})`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spyret",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Headless interfaces between standard Pyret and Spytial",
5
5
  "license": "MIT",
6
6
  "author": "Siddhartha Prasad",