opencode-gbuild 0.4.0 → 0.6.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/README.md +59 -41
- package/agents/gbuild-auditor.md +1 -1
- package/agents/gbuild-reviewer.md +17 -10
- package/cypher/complete.cypher +23 -0
- package/cypher/dispatch.cypher +13 -0
- package/cypher/feature.cypher +21 -0
- package/cypher/node.cypher +39 -0
- package/cypher/record-review.cypher +17 -0
- package/cypher/schema.cypher +45 -0
- package/cypher/set-status.cypher +11 -0
- package/cypher/status.cypher +33 -0
- package/cypher/validate.cypher +105 -0
- package/package.json +2 -2
- package/reference/checklist.md +16 -16
- package/reference/cypher.md +102 -0
- package/reference/failure-policies.md +6 -6
- package/reference/graph-format.md +148 -172
- package/reference/shapes.md +10 -9
- package/skills/plan/SKILL.md +113 -99
- package/skills/pr/SKILL.md +5 -3
- package/skills/review/SKILL.md +15 -7
- package/skills/run/SKILL.md +97 -56
- package/skills/status/SKILL.md +35 -45
- package/templates/example.cypher +57 -0
- package/templates/graph.json.lock +0 -0
- package/scripts/graph.py +0 -231
- package/scripts/test_graph.py +0 -379
- package/scripts/validate_format.py +0 -369
- package/templates/graph.json +0 -110
package/skills/status/SKILL.md
CHANGED
|
@@ -4,6 +4,9 @@ description: Reports a gbuild feature's graph — frontier, blocked, in-flight,
|
|
|
4
4
|
|
|
5
5
|
Report status for `$ARGUMENTS`.
|
|
6
6
|
|
|
7
|
+
Read `${CLAUDE_PLUGIN_ROOT}/reference/cypher.md` first and run its version check. This skill only reads
|
|
8
|
+
the store; `DB` below is `.gbuild/<feature>/db`.
|
|
9
|
+
|
|
7
10
|
## Mode
|
|
8
11
|
|
|
9
12
|
| arguments | mode |
|
|
@@ -15,70 +18,57 @@ Report status for `$ARGUMENTS`.
|
|
|
15
18
|
|
|
16
19
|
### 1. Load
|
|
17
20
|
|
|
18
|
-
`.gbuild/<feature>/graph.json` (a CypherLite GraphData file — GbuildNodes have a `slug` property and are
|
|
19
|
-
linked by `DEPENDS_ON` relationships; feature-level acceptance is `Acceptance` nodes linked by
|
|
20
|
-
`HAS_ACCEPTANCE`/`SATISFIES`) and every `.gbuild/<feature>/nodes/*.json` checkpoint (keyed by slug). This
|
|
21
|
-
is a small, bounded read — status doesn't scale with graph size the way loading full node prose would.
|
|
22
|
-
|
|
23
|
-
### 2. Compute
|
|
24
|
-
|
|
25
21
|
```
|
|
26
|
-
|
|
22
|
+
cypherlite DB -json < ${CLAUDE_PLUGIN_ROOT}/cypher/status.cypher
|
|
23
|
+
cypherlite DB -json < ${CLAUDE_PLUGIN_ROOT}/cypher/validate.cypher
|
|
27
24
|
```
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
One call after the other. `status.cypher` gives one row per node with its derived `state`
|
|
27
|
+
(`frontier` / `blocked` / `in_progress` / `completed` / `cancelled` / `failed`), `wave`, `waiting_on`,
|
|
28
|
+
review summary and timestamps — don't re-derive any of these by hand. This is a small, bounded read —
|
|
29
|
+
status doesn't scale with graph size the way loading full node prose would.
|
|
31
30
|
|
|
32
|
-
###
|
|
31
|
+
### 2. Render the graph
|
|
33
32
|
|
|
34
|
-
Draw the DAG with a marker per node
|
|
33
|
+
Draw the DAG with a marker per node state:
|
|
35
34
|
|
|
36
35
|
```
|
|
37
|
-
✓ completed ◐ in_progress ○
|
|
36
|
+
✓ completed ◐ in_progress ○ frontier ● blocked ✗ failed – cancelled
|
|
38
37
|
```
|
|
39
38
|
|
|
40
|
-
Annotate blocked nodes with what they're waiting on:
|
|
39
|
+
Annotate blocked nodes with what they're waiting on (`waiting_on`):
|
|
40
|
+
`● d-join-and-sum ← b-consume-doubled, c-consume-squared`.
|
|
41
41
|
|
|
42
|
-
Group by wave
|
|
42
|
+
Group by `wave` so fan-out is visible in the layout, not just in the dependency list.
|
|
43
43
|
|
|
44
|
-
###
|
|
44
|
+
### 3. Review verdicts
|
|
45
45
|
|
|
46
|
-
For every `completed` node, one line:
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
For every `completed` node, one line: passed outright (`review_attempts: 1`), or passed after N repair
|
|
47
|
+
attempts. For `failed` nodes, `last_failed_criteria` verbatim — this is what a human needs to make the
|
|
48
|
+
`escalate`/`stop` call.
|
|
49
49
|
|
|
50
|
-
###
|
|
50
|
+
### 4. Concurrency evidence
|
|
51
51
|
|
|
52
|
-
For each wave with more than one node, compare `started_at`
|
|
53
|
-
Report whether they actually overlapped (real concurrent dispatch) or ran
|
|
54
|
-
the same wave (a sign the fallback backend was invoked one node at a time —
|
|
55
|
-
driven, not a graph problem).
|
|
52
|
+
For each wave with more than one completed node, compare the implementing agents' `started_at` /
|
|
53
|
+
`completed_at` intervals. Report whether they actually overlapped (real concurrent dispatch) or ran
|
|
54
|
+
back-to-back despite being in the same wave (a sign the fallback backend was invoked one node at a time —
|
|
55
|
+
a bug in how `run` was driven, not a graph problem).
|
|
56
56
|
|
|
57
|
-
###
|
|
57
|
+
### 5. Next action
|
|
58
58
|
|
|
59
59
|
One priority-ordered list: what `/gbuild:run <feature>` would do next (the current frontier), and
|
|
60
|
-
anything needing a human decision first (`failed
|
|
61
|
-
nodes
|
|
62
|
-
|
|
63
|
-
### 7. Invariant checks
|
|
64
|
-
|
|
65
|
-
Numbered, only reporting violations (silence on an item means it passed, not that it was skipped):
|
|
60
|
+
anything needing a human decision first (`failed` nodes, an empty frontier with incomplete nodes
|
|
61
|
+
remaining, `in_progress` nodes when no run is active — an interrupted wave `run` will reset).
|
|
66
62
|
|
|
67
|
-
|
|
68
|
-
validation — report if a node manages to lack it anyway, since that means the file was hand-edited or
|
|
69
|
-
written by something other than `plan`).
|
|
70
|
-
2. Every `Acceptance` node is `SATISFIES`-covered by at least one GbuildNode (enforced by
|
|
71
|
-
`validate_format.py` — report if one slips through anyway).
|
|
72
|
-
3. `DEPENDS_ON` is acyclic (validated by `validate_format.py`/`graph.py` already; report if it somehow
|
|
73
|
-
wasn't checked).
|
|
74
|
-
4. No `completed` node's checkpoint is missing a `review` field with `verdict: pass` — a node marked
|
|
75
|
-
complete without a passing review means `run` skipped the review step.
|
|
76
|
-
5. Every `chore`-type GbuildNode is the target of at least one `DEPENDS_ON` edge.
|
|
63
|
+
### 6. Invariant checks
|
|
77
64
|
|
|
78
|
-
|
|
79
|
-
|
|
65
|
+
`validate.cypher` is the invariant check — it covers non-empty acceptance, acceptance coverage,
|
|
66
|
+
acyclicity, completed nodes without a passing review or missing an output value, orphan chores, and the
|
|
67
|
+
`FROM` cut test. Report every row it returned, numbered. If it returned `[]`, say so in one line — don't
|
|
68
|
+
let silence be ambiguous between "checked, clear" and "not checked."
|
|
80
69
|
|
|
81
70
|
## All-features mode
|
|
82
71
|
|
|
83
|
-
For each `.gbuild/<slug>/
|
|
84
|
-
frontier is empty (done or stuck) or has ready work. Point at
|
|
72
|
+
For each `.gbuild/<slug>/db` found, run `status.cypher` — one store at a time — and print one line: slug,
|
|
73
|
+
node counts by state, and whether the frontier is empty (done or stuck) or has ready work. Point at
|
|
74
|
+
`/gbuild:status <slug>` for detail.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// A worked 4-node diamond: A produces a value, B and C each consume it
|
|
2
|
+
// independently, D joins both. This is exactly the script plan writes for a
|
|
3
|
+
// new feature — one atomic statement, piped into the store:
|
|
4
|
+
// cypherlite .gbuild/<slug>/db < plan.cypher
|
|
5
|
+
// Variables (f, a1, a, a_value, ...) are only scoped to this one statement, so
|
|
6
|
+
// the whole graph is created in a single CREATE chain.
|
|
7
|
+
.begin
|
|
8
|
+
CREATE (f:Feature {
|
|
9
|
+
feature: 'example-diamond',
|
|
10
|
+
destination: 'A tiny worked example: A produces a shared value, B and C each consume it independently, D joins both.',
|
|
11
|
+
context: "Ships with the plugin as the toy graph used in gbuild's own tests. Not a real feature.",
|
|
12
|
+
out_of_scope: ['Anything beyond demonstrating fan-out and join scheduling.']
|
|
13
|
+
})
|
|
14
|
+
CREATE (a1:Acceptance {id: 'a-1', text: 'run dispatches B and C concurrently, not one after the other'})
|
|
15
|
+
CREATE (a2:Acceptance {id: 'a-2', text: 'D does not start until both B and C are recorded completed'})
|
|
16
|
+
CREATE (f)-[:HAS_ACCEPTANCE]->(a1), (f)-[:HAS_ACCEPTANCE]->(a2)
|
|
17
|
+
|
|
18
|
+
CREATE (a:GbuildNode:Chore {
|
|
19
|
+
slug: 'a-produce-shared-value', title: 'Produce the shared value B and C both need', type: 'chore',
|
|
20
|
+
acceptance: ['output.value is a single integer'],
|
|
21
|
+
failure_policy: 'retry', model_tier: 'cheap', status: 'pending'
|
|
22
|
+
})
|
|
23
|
+
CREATE (a)-[:OUTPUT]->(a_value:Field {name: 'value', shape: '<integer>'})
|
|
24
|
+
|
|
25
|
+
CREATE (b:GbuildNode:Code {
|
|
26
|
+
slug: 'b-consume-doubled', title: 'Consume the shared value, produce its double', type: 'code',
|
|
27
|
+
acceptance: ['output.doubled equals input.value multiplied by 2'],
|
|
28
|
+
failure_policy: 'repair', model_tier: 'cheap', status: 'pending'
|
|
29
|
+
})
|
|
30
|
+
CREATE (b)-[:DEPENDS_ON]->(a)
|
|
31
|
+
CREATE (b)-[:INPUT]->(:Field {name: 'value', shape: '<integer>'})-[:FROM]->(a_value)
|
|
32
|
+
CREATE (b)-[:OUTPUT]->(b_doubled:Field {name: 'doubled', shape: '<integer>'})
|
|
33
|
+
CREATE (b)-[:SATISFIES]->(a1)
|
|
34
|
+
|
|
35
|
+
CREATE (c:GbuildNode:Code {
|
|
36
|
+
slug: 'c-consume-squared', title: 'Consume the shared value, produce its square', type: 'code',
|
|
37
|
+
acceptance: ['output.squared equals input.value raised to the power of 2'],
|
|
38
|
+
failure_policy: 'repair', model_tier: 'cheap', status: 'pending'
|
|
39
|
+
})
|
|
40
|
+
CREATE (c)-[:DEPENDS_ON]->(a)
|
|
41
|
+
CREATE (c)-[:INPUT]->(:Field {name: 'value', shape: '<integer>'})-[:FROM]->(a_value)
|
|
42
|
+
CREATE (c)-[:OUTPUT]->(c_squared:Field {name: 'squared', shape: '<integer>'})
|
|
43
|
+
CREATE (c)-[:SATISFIES]->(a1)
|
|
44
|
+
|
|
45
|
+
CREATE (d:GbuildNode:Code {
|
|
46
|
+
slug: 'd-join-and-sum', title: 'Join B and C, produce their sum', type: 'code',
|
|
47
|
+
acceptance: ['output.sum equals input.doubled plus input.squared',
|
|
48
|
+
'only runs after both b-consume-doubled and c-consume-squared are recorded completed'],
|
|
49
|
+
failure_policy: 'repair', model_tier: 'cheap', status: 'pending'
|
|
50
|
+
})
|
|
51
|
+
CREATE (d)-[:DEPENDS_ON]->(b), (d)-[:DEPENDS_ON]->(c)
|
|
52
|
+
CREATE (d)-[:INPUT]->(:Field {name: 'doubled', shape: '<integer>'})-[:FROM]->(b_doubled)
|
|
53
|
+
CREATE (d)-[:INPUT]->(:Field {name: 'squared', shape: '<integer>'})-[:FROM]->(c_squared)
|
|
54
|
+
CREATE (d)-[:OUTPUT]->(:Field {name: 'sum', shape: '<integer>'})
|
|
55
|
+
CREATE (d)-[:SATISFIES]->(a2);
|
|
56
|
+
.commit
|
|
57
|
+
.checkpoint
|
|
File without changes
|
package/scripts/graph.py
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""gbuild graph utility: validate, topo-order, and query a graph.json feature graph.
|
|
3
|
-
|
|
4
|
-
graph.json is a CypherLite GraphData file — openable with `cypherlite <path>` and
|
|
5
|
-
queryable with Cypher. This module is the query layer (topo waves, frontier,
|
|
6
|
-
status report) over that file. Format validation lives in validate_format.py
|
|
7
|
-
(the single source of truth for the contract); this module imports and re-exports
|
|
8
|
-
its validate() and enum sets.
|
|
9
|
-
|
|
10
|
-
Stdlib only, no pip install required. Schema: ../reference/graph-format.md
|
|
11
|
-
|
|
12
|
-
CLI:
|
|
13
|
-
python3 graph.py <graph.json> --waves print the topological wave decomposition
|
|
14
|
-
python3 graph.py <graph.json> --frontier print node ids ready to run right now
|
|
15
|
-
python3 graph.py <graph.json> --status print the full status report (default)
|
|
16
|
-
|
|
17
|
-
Checkpoints are read from <graph dir>/nodes/<slug>.json by default, or --state-dir.
|
|
18
|
-
"""
|
|
19
|
-
import argparse
|
|
20
|
-
import json
|
|
21
|
-
import sys
|
|
22
|
-
from pathlib import Path
|
|
23
|
-
|
|
24
|
-
# Format contract + enum sets live in validate_format (single definition site).
|
|
25
|
-
from validate_format import (
|
|
26
|
-
GraphError,
|
|
27
|
-
SATISFIED_STATUSES,
|
|
28
|
-
VALID_FAILURE_POLICIES,
|
|
29
|
-
VALID_MODEL_TIERS,
|
|
30
|
-
VALID_NODE_TYPES,
|
|
31
|
-
VALID_TYPE_LABELS,
|
|
32
|
-
validate,
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# --- helpers over the GraphData shape ---------------------------------------
|
|
37
|
-
|
|
38
|
-
def _labels(node):
|
|
39
|
-
return set(node.get("labels", []))
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _props(node):
|
|
43
|
-
return node.get("properties", {}) if isinstance(node.get("properties"), dict) else {}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def gbuild_nodes(graph):
|
|
47
|
-
"""GbuildNode-labelled nodes, as a list."""
|
|
48
|
-
return [n for n in graph.get("nodes", []) if "GbuildNode" in _labels(n)]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def node_by_id(graph, nid):
|
|
52
|
-
for n in graph.get("nodes", []):
|
|
53
|
-
if n.get("id") == nid:
|
|
54
|
-
return n
|
|
55
|
-
return None
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def node_by_slug(graph, slug):
|
|
59
|
-
for n in gbuild_nodes(graph):
|
|
60
|
-
if _props(n).get("slug") == slug:
|
|
61
|
-
return n
|
|
62
|
-
return None
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def slug_of(graph, nid):
|
|
66
|
-
n = node_by_id(graph, nid)
|
|
67
|
-
return _props(n).get("slug") if n else None
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def id_of_slug(graph, slug):
|
|
71
|
-
n = node_by_slug(graph, slug)
|
|
72
|
-
return n.get("id") if n else None
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def deps(graph, node_id):
|
|
76
|
-
"""node_ids this node depends on (DEPENDS_ON end_nodes)."""
|
|
77
|
-
return [
|
|
78
|
-
r["end_node"]
|
|
79
|
-
for r in graph.get("relationships", [])
|
|
80
|
-
if r.get("type") == "DEPENDS_ON" and r.get("start_node") == node_id
|
|
81
|
-
]
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def satisfies(graph, node_id):
|
|
85
|
-
"""acceptance node_ids this node covers (SATISFIES end_nodes)."""
|
|
86
|
-
return [
|
|
87
|
-
r["end_node"]
|
|
88
|
-
for r in graph.get("relationships", [])
|
|
89
|
-
if r.get("type") == "SATISFIES" and r.get("start_node") == node_id
|
|
90
|
-
]
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def feature(graph):
|
|
94
|
-
"""the single Feature node, or None."""
|
|
95
|
-
for n in graph.get("nodes", []):
|
|
96
|
-
if "Feature" in _labels(n):
|
|
97
|
-
return n
|
|
98
|
-
return None
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def acceptance_nodes(graph):
|
|
102
|
-
return [n for n in graph.get("nodes", []) if "Acceptance" in _labels(n)]
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# --- core query layer --------------------------------------------------------
|
|
106
|
-
|
|
107
|
-
def load_graph(path):
|
|
108
|
-
with open(path) as f:
|
|
109
|
-
return json.load(f)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def topo_waves(graph):
|
|
113
|
-
"""Layer GbuildNodes into parallel-safe waves by slug.
|
|
114
|
-
|
|
115
|
-
wave N contains every node whose DEPENDS_ON targets all resolved in <N.
|
|
116
|
-
Returns a list of lists of slugs.
|
|
117
|
-
"""
|
|
118
|
-
gn = gbuild_nodes(graph)
|
|
119
|
-
# map node id -> slug, and slug -> dep slugs
|
|
120
|
-
id_to_slug = {n["id"]: _props(n).get("slug") for n in gn}
|
|
121
|
-
remaining = {id_to_slug[n["id"]]: n for n in gn}
|
|
122
|
-
resolved = set()
|
|
123
|
-
waves = []
|
|
124
|
-
|
|
125
|
-
while remaining:
|
|
126
|
-
wave = []
|
|
127
|
-
for slug, node in remaining.items():
|
|
128
|
-
dep_ids = deps(graph, node["id"])
|
|
129
|
-
dep_slugs = {id_to_slug.get(d) for d in dep_ids}
|
|
130
|
-
if dep_slugs.issubset(resolved):
|
|
131
|
-
wave.append(slug)
|
|
132
|
-
if not wave:
|
|
133
|
-
raise GraphError(f"cycle detected among: {', '.join(sorted(remaining))}")
|
|
134
|
-
wave.sort()
|
|
135
|
-
waves.append(wave)
|
|
136
|
-
for slug in wave:
|
|
137
|
-
resolved.add(slug)
|
|
138
|
-
del remaining[slug]
|
|
139
|
-
|
|
140
|
-
return waves
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def read_checkpoint(state_dir, slug):
|
|
144
|
-
path = Path(state_dir) / f"{slug}.json"
|
|
145
|
-
if not path.exists():
|
|
146
|
-
return {"status": "pending"}
|
|
147
|
-
with open(path) as f:
|
|
148
|
-
return json.load(f)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def node_statuses(graph, state_dir):
|
|
152
|
-
"""slug -> status, for every GbuildNode."""
|
|
153
|
-
return {
|
|
154
|
-
_props(n).get("slug"): read_checkpoint(state_dir, _props(n).get("slug")).get("status", "pending")
|
|
155
|
-
for n in gbuild_nodes(graph)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def compute_report(graph, state_dir):
|
|
160
|
-
statuses = node_statuses(graph, state_dir)
|
|
161
|
-
id_slug = {n["id"]: _props(n).get("slug") for n in gbuild_nodes(graph)}
|
|
162
|
-
frontier, blocked, in_flight, completed, cancelled, failed = [], [], [], [], [], []
|
|
163
|
-
|
|
164
|
-
for node in gbuild_nodes(graph):
|
|
165
|
-
slug = _props(node).get("slug")
|
|
166
|
-
status = statuses.get(slug, "pending")
|
|
167
|
-
dep_ids = deps(graph, node["id"])
|
|
168
|
-
dep_statuses = [statuses.get(id_slug.get(d), "pending") for d in dep_ids]
|
|
169
|
-
if status == "completed":
|
|
170
|
-
completed.append(slug)
|
|
171
|
-
elif status == "cancelled":
|
|
172
|
-
cancelled.append(slug)
|
|
173
|
-
elif status == "in_progress":
|
|
174
|
-
in_flight.append(slug)
|
|
175
|
-
elif status == "failed":
|
|
176
|
-
failed.append(slug)
|
|
177
|
-
elif all(ds in SATISFIED_STATUSES for ds in dep_statuses):
|
|
178
|
-
frontier.append(slug)
|
|
179
|
-
else:
|
|
180
|
-
blocked.append(slug)
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
"frontier": sorted(frontier),
|
|
184
|
-
"blocked": sorted(blocked),
|
|
185
|
-
"in_flight": sorted(in_flight),
|
|
186
|
-
"completed": sorted(completed),
|
|
187
|
-
"cancelled": sorted(cancelled),
|
|
188
|
-
"failed": sorted(failed),
|
|
189
|
-
"waves": topo_waves(graph),
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def compute_frontier(graph, state_dir):
|
|
194
|
-
return compute_report(graph, state_dir)["frontier"]
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def default_state_dir(graph_path):
|
|
198
|
-
return Path(graph_path).resolve().parent / "nodes"
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def main(argv=None):
|
|
202
|
-
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
203
|
-
parser.add_argument("graph_path")
|
|
204
|
-
parser.add_argument("--state-dir", default=None, help="checkpoint dir, default: <graph dir>/nodes")
|
|
205
|
-
group = parser.add_mutually_exclusive_group()
|
|
206
|
-
group.add_argument("--waves", action="store_true")
|
|
207
|
-
group.add_argument("--frontier", action="store_true")
|
|
208
|
-
group.add_argument("--status", action="store_true")
|
|
209
|
-
args = parser.parse_args(argv)
|
|
210
|
-
|
|
211
|
-
graph = load_graph(args.graph_path)
|
|
212
|
-
try:
|
|
213
|
-
validate(graph)
|
|
214
|
-
except GraphError as e:
|
|
215
|
-
print(f"invalid graph: {e}", file=sys.stderr)
|
|
216
|
-
return 1
|
|
217
|
-
|
|
218
|
-
state_dir = args.state_dir or default_state_dir(args.graph_path)
|
|
219
|
-
|
|
220
|
-
if args.waves:
|
|
221
|
-
print(json.dumps(topo_waves(graph)))
|
|
222
|
-
elif args.frontier:
|
|
223
|
-
print(json.dumps(compute_frontier(graph, state_dir)))
|
|
224
|
-
else:
|
|
225
|
-
print(json.dumps(compute_report(graph, state_dir), indent=2))
|
|
226
|
-
|
|
227
|
-
return 0
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if __name__ == "__main__":
|
|
231
|
-
sys.exit(main())
|