diff --git a/skills/add-spytial-python/SKILL.md b/skills/add-spytial-python/SKILL.md new file mode 100644 index 0000000..0c3b1cd --- /dev/null +++ b/skills/add-spytial-python/SKILL.md @@ -0,0 +1,51 @@ +--- +name: add-spytial-python +description: Integrate sPyTial into Python programs with a high-quality authoring workflow. Use when users ask to add, tune, or debug spytial diagram/evaluator usage, selector-driven constraints and directives, CLRS-style data-structure layouts, sequence diagrams, or custom relationalizers in Python codebases and notebooks. +--- + +# Add sPyTial (Python) + +Use this skill to produce a polished, low-friction authoring experience for Python users adopting sPyTial. + +## Workflow + +1. Establish target and constraints. +- Identify structure shape, desired rendering mode (`inline`, `browser`, `file`), and whether the user needs single snapshot or sequence playback. +- If shape matches standard structures, load [CLRS Patterns](references/clrs-patterns.md) and choose the closest template first. + +2. Land a minimal working integration before styling. +- Add a tiny baseline with `spytial.evaluate(obj)` and `spytial.diagram(obj)`. +- Keep first patch runnable with one clear entrypoint. +- If starting from scratch, scaffold with: + `python scripts/scaffold_spytial_starter.py --shape --out ` + +3. Add operations incrementally. +- Add one operation at a time (`orientation`, `align`, `group`, `attribute`, `inferredEdge`, `hideField`, `hideAtom`, `edgeColor`). +- Validate selectors against actual serialized data. +- Load [Selector Cheatsheet](references/selector-cheatsheet.md) for expression syntax and debugging patterns. + +4. Use custom relationalizers only when needed. +- Prefer built-in relationalizers first. +- If semantics are still wrong, implement `RelationalizerBase` with `@relationalizer(priority=100+)`. +- Validate serialization with `evaluate()` before tuning layout. +- Load [Relationalizer Workflow](references/relationalizer-workflow.md). + +5. Finish with verifiable handoff. +- Show run command and expected output artifact. +- If working in this repo, run scoped verification in `spytial-py` (`pytest` or targeted tests). +- Summarize changed files, chosen pattern, selector assumptions, and next tuning options. + +## Quality Bar + +- Deliver runnable code, not pseudocode. +- Preserve a fast feedback loop: `evaluate()` then `diagram()`. +- Prefer CLRS-derived patterns for common data structures. +- Explain non-obvious selectors inline with short comments. +- State assumptions explicitly when the data model is ambiguous. + +## References + +- [Authoring Workflow](references/authoring-workflow.md) +- [CLRS Patterns](references/clrs-patterns.md) +- [Selector Cheatsheet](references/selector-cheatsheet.md) +- [Relationalizer Workflow](references/relationalizer-workflow.md) diff --git a/skills/add-spytial-python/agents/openai.yaml b/skills/add-spytial-python/agents/openai.yaml new file mode 100644 index 0000000..0a7e8fb --- /dev/null +++ b/skills/add-spytial-python/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Add sPyTial (Python)" + short_description: "Help users add sPyTial to Python programs" + default_prompt: "Use $add-spytial-python to integrate sPyTial into my Python project with clear examples and constraints." diff --git a/skills/add-spytial-python/references/authoring-workflow.md b/skills/add-spytial-python/references/authoring-workflow.md new file mode 100644 index 0000000..dfe4d51 --- /dev/null +++ b/skills/add-spytial-python/references/authoring-workflow.md @@ -0,0 +1,68 @@ +# Authoring Workflow + +Use this workflow to integrate sPyTial in a way that stays fast to iterate and easy to maintain. + +## 1. Start with a serialization checkpoint + +Use `evaluate()` before layout tuning: + +```python +import spytial + +spytial.evaluate(obj) +spytial.diagram(obj) +``` + +`evaluate()` confirms atoms/relations. `diagram()` confirms layout. + +## 2. Choose output mode intentionally + +- Notebook workflows: prefer default/`inline`. +- Script + local debugging: prefer `browser`. +- CI/docs artifacts: prefer `file` and commit generated screenshots only if needed. + +```python +spytial.diagram(obj, method="browser") +spytial.diagram(obj, method="file", auto_open=False) +``` + +## 3. Layer operations gradually + +Apply operations in this order unless a use case requires otherwise: + +1. Structural constraints: `orientation`, `align`, `group` +2. Visibility controls: `hideField`, `hideAtom` +3. Readability directives: `attribute`, `tag`, `edgeColor`, `atomColor` +4. Derived edges: `inferredEdge` + +After each layer, rerun `evaluate()` or inspect diagram output before adding more. + +## 4. Sequence diagrams: decide identity policy early + +Use `diagramSequence()` for state transitions. + +- Reused mutable objects across frames: often no identity hook needed. +- Rebuilt objects per frame: pass `identity=...` to stabilize IDs. + +```python +spytial.diagramSequence( + states, + sequence_policy="stability", + identity=lambda obj: obj.id if hasattr(obj, "id") else None, +) +``` + +## 5. Introduce custom relationalizers only if built-ins miss semantics + +Built-ins already cover primitives, `dict`/`list`/`tuple`/`set`, dataclasses, and generic objects. +Use a custom relationalizer when domain meaning is not captured. + +Use priority `>=100` for custom relationalizers. + +## 6. Acceptance checklist + +- `evaluate()` output matches expected domain structure. +- Diagram uses at least one intentional structure cue (direction, alignment, grouping, or inferred edge). +- Selectors are valid and readable. +- Output mode matches user context (notebook/script/CI). +- If sequence: identity behavior is explicit and stable. diff --git a/skills/add-spytial-python/references/clrs-patterns.md b/skills/add-spytial-python/references/clrs-patterns.md new file mode 100644 index 0000000..d7f94b4 --- /dev/null +++ b/skills/add-spytial-python/references/clrs-patterns.md @@ -0,0 +1,112 @@ +# CLRS Patterns + +Use these patterns as first defaults for standard data structures before inventing new selectors. + +## Linked structures (stack/queue/list) + +Recommended defaults: + +- Linear flow: `orientation(selector="next", directions=["directlyRight"])` +- Show payload: `attribute(field="data")` +- Hide sentinels/primitive noise: `hideAtom(selector="NoneType + int + str")` (adjust per model) + +```python +import spytial + +@spytial.orientation(selector="next", directions=["directlyRight"]) +@spytial.attribute(field="data") +class Node: + def __init__(self, data, nxt=None): + self.data = data + self.next = nxt +``` + +## Trees (BST/RB/heap-like) + +Recommended defaults: + +- Left branch: `orientation(..., ["below", "left"])` +- Right branch: `orientation(..., ["below", "right"])` +- Optional sibling alignment: `align(..., direction="horizontal")` +- Surface key metadata with `attribute(field="key")` + +```python +import spytial + +@spytial.orientation(selector="left & (TreeNode->TreeNode)", directions=["below", "left"]) +@spytial.orientation(selector="right & (TreeNode->TreeNode)", directions=["below", "right"]) +@spytial.attribute(field="key") +class TreeNode: + def __init__(self, key, left=None, right=None): + self.key = key + self.left = left + self.right = right +``` + +## Graphs from adjacency structures + +Recommended defaults: + +- Derive explicit edges with `inferredEdge` +- Hide raw container atoms (`list`, tuples, helper wrappers) +- Keep node labels through `attribute(field="key")` or domain field names + +```python +import spytial + +graph = spytial.inferredEdge( + selector="{a, b : Node | b in a.neighbors}", + name="edge", +)(graph_obj) +graph = spytial.hideAtom(selector="list + tuple")(graph) +spytial.diagram(graph) +``` + +## Hash-table and bucketed layouts + +Recommended defaults: + +- Group buckets with selector-based `group(...)` +- Orient chain relations directly left/right +- Hide housekeeping fields such as `prev` where needed + +Selectors from CLRS-style examples often look like: + +- `group(selector="(NoneType.~key) - ((iden & next).Node)", name="T")` +- `orientation(selector="next & (Node->Node)", directions=["directlyRight"])` + +## Matrix / DP table layouts + +Recommended defaults: + +- Build row and column selectors +- `align` rows and columns +- Add directional orientation across row/column deltas + +Pattern from memoization examples: + +- `align(selector=SAME_ROW, direction="horizontal")` +- `align(selector=SAME_COL, direction="vertical")` +- `orientation(selector=DIFF_ROWS, directions=["below"])` +- `orientation(selector=DIFF_COLS, directions=["right"])` + +## Disjoint sets / grouped regions + +Recommended defaults: + +- Group by selector to expose set membership +- Hide technical atoms (`int`, helper lists) after structure checks +- Keep representative fields visible with `attribute(...)` + +## Picking the closest notebook + +Map use case to source notebook: + +- Stacks/queues: `spytial-clrs/src/stacksqueues.ipynb` +- Linked lists: `spytial-clrs/src/linked-lists.ipynb` +- Heaps: `spytial-clrs/src/heaps.ipynb` +- Trees: `spytial-clrs/src/trees.ipynb` +- Hash tables: `spytial-clrs/src/hash-tables.ipynb` +- Graphs: `spytial-clrs/src/graphs.ipynb` +- Disjoint sets: `spytial-clrs/src/disjoint-sets.ipynb` +- Memoization tables: `spytial-clrs/src/memoization.ipynb` diff --git a/skills/add-spytial-python/references/relationalizer-workflow.md b/skills/add-spytial-python/references/relationalizer-workflow.md new file mode 100644 index 0000000..e6d302b --- /dev/null +++ b/skills/add-spytial-python/references/relationalizer-workflow.md @@ -0,0 +1,58 @@ +# Relationalizer Workflow + +Use this only when built-in relationalizers cannot express domain semantics cleanly. + +## Decision rule + +Implement a custom relationalizer when at least one is true: + +- Built-in output merges concepts that should be separate node types. +- Required domain edges do not exist in serialized output. +- You need stable, explicit IDs/labels not derivable from defaults. + +Stay with built-ins when you only need visual/layout tuning. + +## Minimal implementation + +```python +from spytial import RelationalizerBase, relationalizer, Atom, Relation + +@relationalizer(priority=100) +class WidgetRelationalizer(RelationalizerBase): + def can_handle(self, obj): + return hasattr(obj, "widget_id") + + def relationalize(self, obj, walker_func): + widget_atom = Atom( + id=f"widget:{obj.widget_id}", + type="Widget", + label=getattr(obj, "name", str(obj.widget_id)), + ) + rels = [] + if getattr(obj, "parent", None) is not None: + parent_id = walker_func._get_id(obj.parent) + rels.append(Relation(name="parent", tuples=[(widget_atom.id, parent_id)])) + return [widget_atom], rels +``` + +## Required checks + +1. Import path registers class (decorator executes on import). +2. Priority is `>=100` (built-ins reserve lower range). +3. `spytial.evaluate(sample)` shows expected atoms/relations. +4. `spytial.diagram(sample)` renders without missing-edge surprises. + +## Common mistakes + +- Implementing relationalizer when selectors/operations were enough. +- Returning unstable IDs across runs, breaking sequence/view consistency. +- Skipping `evaluate()` and debugging only in diagram view. +- Forgetting to import module containing the decorated class. + +## Handoff expectations + +When adding a custom relationalizer, include: + +- Why built-ins were insufficient. +- Chosen atom types and relation names. +- One or two selectors that rely on the new relations. diff --git a/skills/add-spytial-python/references/selector-cheatsheet.md b/skills/add-spytial-python/references/selector-cheatsheet.md new file mode 100644 index 0000000..b2ff068 --- /dev/null +++ b/skills/add-spytial-python/references/selector-cheatsheet.md @@ -0,0 +1,86 @@ +# Selector Cheatsheet + +This cheatsheet is based on `simple-graph-query` syntax as used by sPyTial selectors. + +## Core set and relation operators + +- Union: `A + B` +- Intersection: `A & B` +- Difference: `A - B` +- Join: `A.rel` or `rel.Type` +- Product: `A -> B` +- Transpose: `~rel` +- Transitive closure: `^rel` +- Reflexive-transitive closure: `*rel` + +Examples: + +- `Node.key` +- `edge.edge` +- `~parent` +- `^next` +- `Node - NoneType` + +## Predicates and logic + +- Membership: `x in S` +- Equality/inequality: `x = y`, `x != y` +- Boolean ops: `and`, `or`, `!`, `=>`, `<=>` +- Quantifiers: `all`, `some`, `no`, `one`, `lone` + +Examples: + +- `some x: Node | x in roots` +- `all x: Item | some y: Item | x != y` +- `all disj i, j: Int | not i = j` + +## Comprehensions (great for `group`/`orientation` selectors) + +Unary: + +```txt +{x : Item | x.value > 10} +``` + +Binary: + +```txt +{b : Basket, a : Fruit | (a in b.fruit) and a.status = Rotten} +``` + +Numeric ordering (common for array-backed structures): + +```txt +{x, y : idx[object][object] | @num:(x[idx[object]]) < @num:(y[idx[object]])} +``` + +## Built-ins that matter most + +- `univ`: all atoms +- `iden`: identity relation (all `(a, a)` pairs) +- `Int`: integer atoms + +## Label conversion helpers + +- `@:(expr)` convert to string label form +- `@num:(expr)` convert to number for numeric comparisons + +Examples: + +- `@:(n14) = @:(12)` +- `@num:(x[idx[object]]) < @num:(y[idx[object]])` + +## Reserved keyword identifiers + +If a field/type name conflicts with a keyword, use backticks: + +- `` `set` `` +- `` item0.`in` `` + +## Debugging workflow for selectors + +1. Open `spytial.evaluate(obj)` to inspect available atoms/relations. +2. Start with a broad selector (`TypeName` or relation name). +3. Add one operator at a time (`&`, `-`, join, then comprehension). +4. Re-run and verify before using it in an annotation. +5. Keep selectors readable; factor complex expressions into local constants. diff --git a/skills/add-spytial-python/scripts/scaffold_spytial_starter.py b/skills/add-spytial-python/scripts/scaffold_spytial_starter.py new file mode 100644 index 0000000..e99ecff --- /dev/null +++ b/skills/add-spytial-python/scripts/scaffold_spytial_starter.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Generate starter sPyTial integration snippets for common data shapes.""" + +from __future__ import annotations + +import argparse +from pathlib import Path +import textwrap + + +TEMPLATES = { + "linked-list": textwrap.dedent( + """\ + import spytial + + + @spytial.orientation(selector="next", directions=["directlyRight"]) + @spytial.attribute(field="data") + class Node: + def __init__(self, data, nxt=None): + self.data = data + self.next = nxt + + + head = Node(1, Node(2, Node(3))) + + # Validate serialized structure first. + spytial.evaluate(head) + # Then inspect layout. + spytial.diagram(head) + """ + ), + "tree": textwrap.dedent( + """\ + import spytial + + + @spytial.orientation(selector="left & (TreeNode->TreeNode)", directions=["below", "left"]) + @spytial.orientation(selector="right & (TreeNode->TreeNode)", directions=["below", "right"]) + @spytial.attribute(field="key") + class TreeNode: + def __init__(self, key, left=None, right=None): + self.key = key + self.left = left + self.right = right + + + root = TreeNode(8, TreeNode(3, TreeNode(1), TreeNode(6)), TreeNode(10, None, TreeNode(14))) + + spytial.evaluate(root) + spytial.diagram(root) + """ + ), + "graph": textwrap.dedent( + """\ + import spytial + + + class GNode: + def __init__(self, key): + self.key = key + self.neighbors = [] + + + a, b, c = GNode("A"), GNode("B"), GNode("C") + a.neighbors = [b, c] + b.neighbors = [c] + c.neighbors = [a] + nodes = [a, b, c] + + graph = spytial.inferredEdge( + selector="{x : GNode, y : GNode | y in x.neighbors}", + name="edge", + )(nodes) + graph = spytial.hideAtom(selector="list")(graph) + + spytial.evaluate(graph) + spytial.diagram(graph) + """ + ), + "matrix": textwrap.dedent( + """\ + import spytial + + + class Cell: + def __init__(self, row, col, value): + self.row = row + self.col = col + self.value = value + + + class MatrixWrapper: + def __init__(self, rows, cols): + self.cells = [Cell(r, c, r * cols + c) for r in range(rows) for c in range(cols)] + + + SAME_ROW = "{a, b : Cell | a.row = b.row and a != b}" + SAME_COL = "{a, b : Cell | a.col = b.col and a != b}" + NEXT_ROW = "{a, b : Cell | @num:(a.row) + 1 = @num:(b.row) and a.col = b.col}" + NEXT_COL = "{a, b : Cell | @num:(a.col) + 1 = @num:(b.col) and a.row = b.row}" + + + @spytial.align(selector=SAME_ROW, direction="horizontal") + @spytial.align(selector=SAME_COL, direction="vertical") + @spytial.orientation(selector=NEXT_ROW, directions=["below"]) + @spytial.orientation(selector=NEXT_COL, directions=["right"]) + @spytial.attribute(field="value") + class MatrixView(MatrixWrapper): + pass + + + matrix = MatrixView(3, 4) + spytial.evaluate(matrix) + spytial.diagram(matrix) + """ + ), +} + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Generate starter sPyTial integration code for a common data shape." + ) + parser.add_argument( + "--shape", + choices=sorted(TEMPLATES.keys()), + required=True, + help="Starter template to generate.", + ) + parser.add_argument( + "--out", + required=True, + help="Output Python file path.", + ) + parser.add_argument( + "--force", + action="store_true", + help="Overwrite output file if it already exists.", + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + out_path = Path(args.out) + + if out_path.exists() and not args.force: + print(f"Refusing to overwrite existing file: {out_path}") + print("Pass --force to overwrite.") + return 1 + + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(TEMPLATES[args.shape], encoding="utf-8") + print(f"Generated {args.shape} starter at {out_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())