opencode-gbuild 0.5.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 +43 -37
- 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 -183
- package/reference/shapes.md +10 -9
- package/skills/plan/SKILL.md +113 -102
- package/skills/pr/SKILL.md +5 -3
- package/skills/review/SKILL.md +15 -7
- package/skills/run/SKILL.md +97 -57
- package/skills/status/SKILL.md +35 -46
- package/templates/example.cypher +57 -0
- package/scripts/__pycache__/graph.cpython-314.pyc +0 -0
- package/scripts/__pycache__/test_graph.cpython-314.pyc +0 -0
- package/scripts/__pycache__/validate_format.cpython-314.pyc +0 -0
- package/scripts/graph.py +0 -239
- package/scripts/test_graph.py +0 -403
- package/scripts/validate_format.py +0 -378
- package/templates/graph.json +0 -110
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""gbuild graph format validator — the authoritative contract for graph.json.
|
|
3
|
-
|
|
4
|
-
graph.json is a CypherLite GraphData file, living at
|
|
5
|
-
`.gbuild/<feature>/db/graph.json` so CypherLite can open the `db/` folder as a
|
|
6
|
-
JSON-store database (`cypherlite .gbuild/<feature>/db "MATCH ..."`).
|
|
7
|
-
This module checks it is both CypherLite-loadable and gbuild-correct: the
|
|
8
|
-
GraphData envelope, node shapes (Feature / Acceptance / GbuildNode), the three
|
|
9
|
-
relationship types (DEPENDS_ON / HAS_ACCEPTANCE / SATISFIES), id/counter
|
|
10
|
-
invariants, acyclicity, and coverage. Stdlib only — same no-install rule as
|
|
11
|
-
graph.py.
|
|
12
|
-
|
|
13
|
-
Single source of truth for the format: graph.py imports VALID_* and
|
|
14
|
-
SATISFIED_STATUSES from here, and its validate() calls validate() below.
|
|
15
|
-
|
|
16
|
-
CLI:
|
|
17
|
-
python3 validate_format.py <feature>/db/graph.json
|
|
18
|
-
exit 0 + "valid: <feature>" on success
|
|
19
|
-
exit 1 + "invalid: <errors joined>" on failure
|
|
20
|
-
"""
|
|
21
|
-
import argparse
|
|
22
|
-
import json
|
|
23
|
-
import re
|
|
24
|
-
import sys
|
|
25
|
-
from pathlib import Path
|
|
26
|
-
|
|
27
|
-
# --- enum sets (single definition site; graph.py imports these) -------------
|
|
28
|
-
|
|
29
|
-
VALID_NODE_TYPES = {"research", "decision", "code", "test", "verify", "chore"}
|
|
30
|
-
VALID_FAILURE_POLICIES = {"retry", "fallback", "skip", "repair", "escalate", "stop"}
|
|
31
|
-
VALID_MODEL_TIERS = {"cheap", "strong"}
|
|
32
|
-
SATISFIED_STATUSES = {"completed", "cancelled"}
|
|
33
|
-
|
|
34
|
-
# PascalCase type label <-> lowercase type property. Order matches VALID_NODE_TYPES.
|
|
35
|
-
TYPE_TO_LABEL = {
|
|
36
|
-
"research": "Research",
|
|
37
|
-
"decision": "Decision",
|
|
38
|
-
"code": "Code",
|
|
39
|
-
"test": "Test",
|
|
40
|
-
"verify": "Verify",
|
|
41
|
-
"chore": "Chore",
|
|
42
|
-
}
|
|
43
|
-
VALID_TYPE_LABELS = set(TYPE_TO_LABEL.values())
|
|
44
|
-
|
|
45
|
-
VALID_REL_TYPES = {"DEPENDS_ON", "HAS_ACCEPTANCE", "SATISFIES"}
|
|
46
|
-
|
|
47
|
-
SLUG_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
|
|
48
|
-
ACCEPTANCE_ID_RE = re.compile(r"^a-\d+$")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class GraphError(Exception):
|
|
52
|
-
pass
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def _is_int(v):
|
|
56
|
-
return isinstance(v, int) and not isinstance(v, bool)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _is_str_list(v):
|
|
60
|
-
return isinstance(v, list) and all(isinstance(x, str) for x in v)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def validate(graph):
|
|
64
|
-
"""Raise GraphError with every problem found, joined, rather than the first.
|
|
65
|
-
|
|
66
|
-
Collects all structural, shape, relationship, counter, acyclicity, and
|
|
67
|
-
coverage errors into one message so a single validate pass tells the author
|
|
68
|
-
everything to fix.
|
|
69
|
-
"""
|
|
70
|
-
errors = []
|
|
71
|
-
|
|
72
|
-
if not isinstance(graph, dict):
|
|
73
|
-
raise GraphError("graph root is not an object")
|
|
74
|
-
|
|
75
|
-
# --- GraphData envelope ---------------------------------------------------
|
|
76
|
-
for key in ("nodes", "relationships", "indexes", "constraints"):
|
|
77
|
-
if not isinstance(graph.get(key), list):
|
|
78
|
-
errors.append(f"top-level {key!r} must be a list")
|
|
79
|
-
for key in ("next_node_id", "next_rel_id"):
|
|
80
|
-
if not _is_int(graph.get(key)):
|
|
81
|
-
errors.append(f"top-level {key!r} must be an integer")
|
|
82
|
-
|
|
83
|
-
nodes = graph.get("nodes") if isinstance(graph.get("nodes"), list) else []
|
|
84
|
-
rels = graph.get("relationships") if isinstance(graph.get("relationships"), list) else []
|
|
85
|
-
|
|
86
|
-
if not nodes:
|
|
87
|
-
errors.append("graph has no nodes")
|
|
88
|
-
|
|
89
|
-
# --- node shape + id uniqueness ------------------------------------------
|
|
90
|
-
node_ids = set()
|
|
91
|
-
for n in nodes:
|
|
92
|
-
if not isinstance(n, dict):
|
|
93
|
-
errors.append("a node is not an object")
|
|
94
|
-
continue
|
|
95
|
-
nid = n.get("id")
|
|
96
|
-
if not _is_int(nid):
|
|
97
|
-
errors.append("a node has a missing or non-integer id")
|
|
98
|
-
continue
|
|
99
|
-
if nid in node_ids:
|
|
100
|
-
errors.append(f"duplicate node id: {nid}")
|
|
101
|
-
node_ids.add(nid)
|
|
102
|
-
if not isinstance(n.get("labels"), list) or not all(isinstance(l, str) for l in n.get("labels", [])):
|
|
103
|
-
errors.append(f"node {nid}: labels must be a list of strings")
|
|
104
|
-
if not isinstance(n.get("properties"), dict):
|
|
105
|
-
errors.append(f"node {nid}: properties must be an object")
|
|
106
|
-
|
|
107
|
-
by_id = {n["id"]: n for n in nodes if isinstance(n, dict) and _is_int(n.get("id"))}
|
|
108
|
-
labels_by_id = {nid: set(n.get("labels", [])) for nid, n in by_id.items()}
|
|
109
|
-
props_by_id = {nid: n.get("properties", {}) for nid, n in by_id.items() if isinstance(n.get("properties"), dict)}
|
|
110
|
-
|
|
111
|
-
# --- Feature node --------------------------------------------------------
|
|
112
|
-
feature_nodes = [nid for nid, ls in labels_by_id.items() if "Feature" in ls]
|
|
113
|
-
if len(feature_nodes) != 1:
|
|
114
|
-
errors.append(f"expected exactly one Feature node, found {len(feature_nodes)}")
|
|
115
|
-
feature_id = feature_nodes[0] if feature_nodes else None
|
|
116
|
-
if feature_id is not None:
|
|
117
|
-
p = props_by_id.get(feature_id, {})
|
|
118
|
-
for key in ("feature", "destination", "context"):
|
|
119
|
-
if not isinstance(p.get(key), str):
|
|
120
|
-
errors.append(f"Feature node: {key!r} must be a string")
|
|
121
|
-
if not _is_str_list(p.get("out_of_scope")):
|
|
122
|
-
errors.append("Feature node: 'out_of_scope' must be a list of strings")
|
|
123
|
-
if "acceptance" in p:
|
|
124
|
-
errors.append("Feature node must not carry an 'acceptance' property (use Acceptance nodes)")
|
|
125
|
-
|
|
126
|
-
# --- Acceptance nodes ----------------------------------------------------
|
|
127
|
-
acceptance_nodes = {} # id property -> node id
|
|
128
|
-
for nid, ls in labels_by_id.items():
|
|
129
|
-
if "Acceptance" not in ls:
|
|
130
|
-
continue
|
|
131
|
-
p = props_by_id.get(nid, {})
|
|
132
|
-
aid = p.get("id")
|
|
133
|
-
if not isinstance(aid, str) or not ACCEPTANCE_ID_RE.match(aid):
|
|
134
|
-
errors.append(f"node {nid} (Acceptance): 'id' must be a string like 'a-1'")
|
|
135
|
-
elif aid in acceptance_nodes:
|
|
136
|
-
errors.append(f"duplicate Acceptance id: {aid}")
|
|
137
|
-
else:
|
|
138
|
-
acceptance_nodes[aid] = nid
|
|
139
|
-
if not isinstance(p.get("text"), str) or not p.get("text", "").strip():
|
|
140
|
-
errors.append(f"node {nid} (Acceptance): 'text' must be a non-blank string")
|
|
141
|
-
|
|
142
|
-
# --- GbuildNode shape ----------------------------------------------------
|
|
143
|
-
gbuild_nodes = {} # slug -> node id
|
|
144
|
-
for nid, ls in labels_by_id.items():
|
|
145
|
-
if "GbuildNode" not in ls:
|
|
146
|
-
continue
|
|
147
|
-
p = props_by_id.get(nid, {})
|
|
148
|
-
slug = p.get("slug")
|
|
149
|
-
if not isinstance(slug, str) or not SLUG_RE.match(slug):
|
|
150
|
-
errors.append(f"node {nid} (GbuildNode): 'slug' must be a kebab-case string")
|
|
151
|
-
elif slug in gbuild_nodes:
|
|
152
|
-
errors.append(f"duplicate GbuildNode slug: {slug}")
|
|
153
|
-
else:
|
|
154
|
-
gbuild_nodes[slug] = nid
|
|
155
|
-
if not isinstance(p.get("title"), str):
|
|
156
|
-
errors.append(f"node {nid} (GbuildNode): 'title' must be a string")
|
|
157
|
-
ntype = p.get("type")
|
|
158
|
-
if ntype not in VALID_NODE_TYPES:
|
|
159
|
-
errors.append(f"node {nid} (GbuildNode): invalid type {ntype!r}")
|
|
160
|
-
contract = p.get("contract")
|
|
161
|
-
if not isinstance(contract, dict) or not isinstance(contract.get("input"), dict) or not isinstance(contract.get("output"), dict):
|
|
162
|
-
errors.append(f"node {nid} (GbuildNode): 'contract' must be {{input:{{}},output:{{}}}}")
|
|
163
|
-
acc = p.get("acceptance")
|
|
164
|
-
if not isinstance(acc, list) or len(acc) == 0:
|
|
165
|
-
errors.append(f"node {nid} (GbuildNode): 'acceptance' must be a non-empty list")
|
|
166
|
-
elif any(not str(c).strip() for c in acc):
|
|
167
|
-
errors.append(f"node {nid} (GbuildNode): 'acceptance' must not contain blank entries")
|
|
168
|
-
if p.get("verify") is not None and not isinstance(p.get("verify"), str):
|
|
169
|
-
errors.append(f"node {nid} (GbuildNode): 'verify' must be a string or null")
|
|
170
|
-
if p.get("failure_policy") not in VALID_FAILURE_POLICIES:
|
|
171
|
-
errors.append(f"node {nid} (GbuildNode): invalid failure_policy {p.get('failure_policy')!r}")
|
|
172
|
-
if p.get("model_tier") not in VALID_MODEL_TIERS:
|
|
173
|
-
errors.append(f"node {nid} (GbuildNode): invalid model_tier {p.get('model_tier')!r}")
|
|
174
|
-
if "satisfies" in p:
|
|
175
|
-
errors.append(f"node {nid} (GbuildNode): must not carry a 'satisfies' property (use SATISFIES edges)")
|
|
176
|
-
# type-label consistency
|
|
177
|
-
if ntype in TYPE_TO_LABEL:
|
|
178
|
-
expected_label = TYPE_TO_LABEL[ntype]
|
|
179
|
-
if expected_label not in ls:
|
|
180
|
-
errors.append(f"node {nid} (GbuildNode): type {ntype!r} requires label {expected_label!r}")
|
|
181
|
-
extra_type_labels = (ls & VALID_TYPE_LABELS) - {expected_label}
|
|
182
|
-
if extra_type_labels:
|
|
183
|
-
errors.append(f"node {nid} (GbuildNode): type labels {extra_type_labels} do not match type {ntype!r}")
|
|
184
|
-
|
|
185
|
-
gbuild_ids = {nid for nid, ls in labels_by_id.items() if "GbuildNode" in ls}
|
|
186
|
-
|
|
187
|
-
# --- relationship shape --------------------------------------------------
|
|
188
|
-
rel_ids = set()
|
|
189
|
-
depends_edges = [] # (start, end) for acyclicity
|
|
190
|
-
has_acc_ends = set() # acceptance node ids targeted by HAS_ACCEPTANCE
|
|
191
|
-
satisfies_ends = set() # acceptance node ids targeted by SATISFIES
|
|
192
|
-
depends_targets = set() # gbuild node ids that are depended upon (chore coverage)
|
|
193
|
-
for r in rels:
|
|
194
|
-
if not isinstance(r, dict):
|
|
195
|
-
errors.append("a relationship is not an object")
|
|
196
|
-
continue
|
|
197
|
-
rid = r.get("id")
|
|
198
|
-
if not _is_int(rid):
|
|
199
|
-
errors.append("a relationship has a missing or non-integer id")
|
|
200
|
-
continue
|
|
201
|
-
if rid in rel_ids:
|
|
202
|
-
errors.append(f"duplicate relationship id: {rid}")
|
|
203
|
-
rel_ids.add(rid)
|
|
204
|
-
rtype = r.get("type")
|
|
205
|
-
if rtype not in VALID_REL_TYPES:
|
|
206
|
-
errors.append(f"relationship {rid}: invalid type {rtype!r}")
|
|
207
|
-
continue
|
|
208
|
-
s, e = r.get("start_node"), r.get("end_node")
|
|
209
|
-
if s not in by_id:
|
|
210
|
-
errors.append(f"relationship {rid}: start_node {s!r} does not exist")
|
|
211
|
-
if e not in by_id:
|
|
212
|
-
errors.append(f"relationship {rid}: end_node {e!r} does not exist")
|
|
213
|
-
continue
|
|
214
|
-
if not isinstance(r.get("properties"), dict):
|
|
215
|
-
errors.append(f"relationship {rid}: properties must be an object")
|
|
216
|
-
# type-specific endpoint constraints
|
|
217
|
-
if rtype == "DEPENDS_ON":
|
|
218
|
-
if s not in gbuild_ids:
|
|
219
|
-
errors.append(f"relationship {rid} (DEPENDS_ON): start_node {s} must be a GbuildNode")
|
|
220
|
-
if e not in gbuild_ids:
|
|
221
|
-
errors.append(f"relationship {rid} (DEPENDS_ON): end_node {e} must be a GbuildNode")
|
|
222
|
-
else:
|
|
223
|
-
depends_edges.append((s, e))
|
|
224
|
-
depends_targets.add(e)
|
|
225
|
-
elif rtype == "HAS_ACCEPTANCE":
|
|
226
|
-
if feature_id is not None and s != feature_id:
|
|
227
|
-
errors.append(f"relationship {rid} (HAS_ACCEPTANCE): start_node {s} must be the Feature node")
|
|
228
|
-
if "Acceptance" not in labels_by_id.get(e, set()):
|
|
229
|
-
errors.append(f"relationship {rid} (HAS_ACCEPTANCE): end_node {e} must be an Acceptance node")
|
|
230
|
-
else:
|
|
231
|
-
has_acc_ends.add(e)
|
|
232
|
-
elif rtype == "SATISFIES":
|
|
233
|
-
if s not in gbuild_ids:
|
|
234
|
-
errors.append(f"relationship {rid} (SATISFIES): start_node {s} must be a GbuildNode")
|
|
235
|
-
if "Acceptance" not in labels_by_id.get(e, set()):
|
|
236
|
-
errors.append(f"relationship {rid} (SATISFIES): end_node {e} must be an Acceptance node")
|
|
237
|
-
else:
|
|
238
|
-
satisfies_ends.add(e)
|
|
239
|
-
|
|
240
|
-
# --- counter invariant ---------------------------------------------------
|
|
241
|
-
if nodes and _is_int(graph.get("next_node_id")):
|
|
242
|
-
expected_n = max(n["id"] for n in nodes if isinstance(n, dict) and _is_int(n.get("id"))) + 1
|
|
243
|
-
if graph["next_node_id"] != expected_n:
|
|
244
|
-
errors.append(f"next_node_id must be {expected_n} (max node id + 1), got {graph['next_node_id']}")
|
|
245
|
-
if rels and _is_int(graph.get("next_rel_id")):
|
|
246
|
-
expected_r = max(r["id"] for r in rels if isinstance(r, dict) and _is_int(r.get("id"))) + 1
|
|
247
|
-
if graph["next_rel_id"] != expected_r:
|
|
248
|
-
errors.append(f"next_rel_id must be {expected_r} (max rel id + 1), got {graph['next_rel_id']}")
|
|
249
|
-
|
|
250
|
-
# --- acyclicity ---------------------------------------------------------
|
|
251
|
-
if depends_edges and not any("DEPENDS_ON" in e for e in errors):
|
|
252
|
-
cycle = _detect_cycle(gbuild_ids, depends_edges)
|
|
253
|
-
if cycle:
|
|
254
|
-
errors.append(f"cycle detected among: {', '.join(sorted(str(c) for c in cycle))}")
|
|
255
|
-
|
|
256
|
-
# --- coverage -----------------------------------------------------------
|
|
257
|
-
for aid, anid in acceptance_nodes.items():
|
|
258
|
-
if anid not in satisfies_ends:
|
|
259
|
-
errors.append(f"Acceptance {aid!r} (node {anid}) is not covered by any SATISFIES edge")
|
|
260
|
-
if anid not in has_acc_ends:
|
|
261
|
-
errors.append(f"Acceptance {aid!r} (node {anid}) is not linked by any HAS_ACCEPTANCE edge")
|
|
262
|
-
# every chore must be depended on
|
|
263
|
-
for nid in gbuild_ids:
|
|
264
|
-
p = props_by_id.get(nid, {})
|
|
265
|
-
if p.get("type") == "chore" and nid not in depends_targets:
|
|
266
|
-
errors.append(f"chore node {nid} (slug {p.get('slug')!r}) is not depended on by any node")
|
|
267
|
-
|
|
268
|
-
if errors:
|
|
269
|
-
raise GraphError("; ".join(errors))
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
def _detect_cycle(node_ids, edges):
|
|
273
|
-
"""Return a set of node ids involved in a cycle, or empty set if acyclic."""
|
|
274
|
-
adj = {nid: [] for nid in node_ids}
|
|
275
|
-
for s, e in edges:
|
|
276
|
-
adj.setdefault(s, []).append(e)
|
|
277
|
-
WHITE, GRAY, BLACK = 0, 1, 2
|
|
278
|
-
color = {nid: WHITE for nid in node_ids}
|
|
279
|
-
in_cycle = set()
|
|
280
|
-
|
|
281
|
-
def dfs(u, stack):
|
|
282
|
-
color[u] = GRAY
|
|
283
|
-
stack.append(u)
|
|
284
|
-
for v in adj.get(u, []):
|
|
285
|
-
if v not in color:
|
|
286
|
-
continue
|
|
287
|
-
if color[v] == GRAY:
|
|
288
|
-
# cycle: from v back to u along the stack
|
|
289
|
-
idx = stack.index(v)
|
|
290
|
-
in_cycle.update(stack[idx:])
|
|
291
|
-
elif color[v] == WHITE:
|
|
292
|
-
dfs(v, stack)
|
|
293
|
-
stack.pop()
|
|
294
|
-
color[u] = BLACK
|
|
295
|
-
|
|
296
|
-
for nid in node_ids:
|
|
297
|
-
if color.get(nid) == WHITE:
|
|
298
|
-
dfs(nid, [])
|
|
299
|
-
return in_cycle
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
def _cypherlite_cross_check(graph_path):
|
|
303
|
-
"""If cypherlite is on PATH, confirm CypherLite itself loads the graph and
|
|
304
|
-
can run a trivial query. Catches serde/encoding issues the stdlib walk can't
|
|
305
|
-
(e.g. a top-level shape CypherLite's GraphData loader rejects). Returns an
|
|
306
|
-
error string on failure, None on success or when cypherlite is absent.
|
|
307
|
-
|
|
308
|
-
CypherLite opens a *directory* store, not a bare `.json` file. To avoid
|
|
309
|
-
writing a WAL into the plan's `db/` folder during validation, copy the file
|
|
310
|
-
into a throwaway dir as `graph.json` and open that.
|
|
311
|
-
|
|
312
|
-
Absence is silent — cypherlite is an optional consumer, not a dependency.
|
|
313
|
-
"""
|
|
314
|
-
import shutil
|
|
315
|
-
import subprocess
|
|
316
|
-
import tempfile
|
|
317
|
-
bin_path = shutil.which("cypherlite")
|
|
318
|
-
if bin_path is None:
|
|
319
|
-
return None # not installed — skip, not a failure
|
|
320
|
-
with tempfile.TemporaryDirectory() as scratch:
|
|
321
|
-
shutil.copyfile(graph_path, Path(scratch) / "graph.json")
|
|
322
|
-
res = subprocess.run(
|
|
323
|
-
[bin_path, scratch, "-json", "MATCH (n) RETURN count(n) AS c"],
|
|
324
|
-
capture_output=True, text=True, timeout=30,
|
|
325
|
-
)
|
|
326
|
-
# cypherlite returns exit 0 even on some load failures (it prints the error
|
|
327
|
-
# to stderr), so check both the exit code AND the output for failure markers.
|
|
328
|
-
out = (res.stderr + res.stdout).strip()
|
|
329
|
-
if res.returncode != 0:
|
|
330
|
-
return f"cypherlite refused to load the file: {out or 'unknown error'}"
|
|
331
|
-
if "Failed to open database" in out or "failed to parse JSON" in out:
|
|
332
|
-
return f"cypherlite refused to load the file: {out}"
|
|
333
|
-
return None
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
def main(argv=None):
|
|
337
|
-
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
338
|
-
parser.add_argument("graph_path")
|
|
339
|
-
parser.add_argument("--no-cypherlite", action="store_true",
|
|
340
|
-
help="skip the optional cypherlite load cross-check even if the binary is present")
|
|
341
|
-
args = parser.parse_args(argv)
|
|
342
|
-
|
|
343
|
-
try:
|
|
344
|
-
with open(args.graph_path) as f:
|
|
345
|
-
graph = json.load(f)
|
|
346
|
-
except (OSError, json.JSONDecodeError) as e:
|
|
347
|
-
print(f"invalid: cannot read {args.graph_path}: {e}", file=sys.stderr)
|
|
348
|
-
return 1
|
|
349
|
-
|
|
350
|
-
try:
|
|
351
|
-
validate(graph)
|
|
352
|
-
except GraphError as e:
|
|
353
|
-
print(f"invalid: {e}", file=sys.stderr)
|
|
354
|
-
return 1
|
|
355
|
-
|
|
356
|
-
# Optional cross-check: confirm CypherLite itself loads the file (catches
|
|
357
|
-
# serde/encoding issues the stdlib checks above can't). Skipped silently if
|
|
358
|
-
# cypherlite isn't installed — it's an optional consumer, not a dependency.
|
|
359
|
-
if not args.no_cypherlite:
|
|
360
|
-
cl_err = _cypherlite_cross_check(args.graph_path)
|
|
361
|
-
if cl_err is not None:
|
|
362
|
-
print(f"invalid: {cl_err}", file=sys.stderr)
|
|
363
|
-
return 1
|
|
364
|
-
|
|
365
|
-
# best-effort feature slug for the success line
|
|
366
|
-
feature = "?"
|
|
367
|
-
for n in graph.get("nodes", []):
|
|
368
|
-
if isinstance(n, dict) and "Feature" in n.get("labels", []):
|
|
369
|
-
p = n.get("properties", {})
|
|
370
|
-
if isinstance(p.get("feature"), str):
|
|
371
|
-
feature = p["feature"]
|
|
372
|
-
break
|
|
373
|
-
print(f"valid: {feature}")
|
|
374
|
-
return 0
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if __name__ == "__main__":
|
|
378
|
-
sys.exit(main())
|
package/templates/graph.json
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"nodes": [
|
|
3
|
-
{
|
|
4
|
-
"id": 1,
|
|
5
|
-
"labels": ["Feature"],
|
|
6
|
-
"properties": {
|
|
7
|
-
"feature": "example-diamond",
|
|
8
|
-
"destination": "A tiny worked example: A produces a shared value, B and C each consume it independently, D joins both.",
|
|
9
|
-
"context": "Ships with the plugin as the toy graph used in gbuild's own verification steps. Not a real feature.",
|
|
10
|
-
"out_of_scope": ["Anything beyond demonstrating fan-out and join scheduling."]
|
|
11
|
-
}
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
"id": 2,
|
|
15
|
-
"labels": ["Acceptance"],
|
|
16
|
-
"properties": { "id": "a-1", "text": "run dispatches B and C concurrently, not one after the other" }
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
"id": 3,
|
|
20
|
-
"labels": ["Acceptance"],
|
|
21
|
-
"properties": { "id": "a-2", "text": "D does not start until both B and C are checkpointed complete" }
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"id": 4,
|
|
25
|
-
"labels": ["GbuildNode", "Chore"],
|
|
26
|
-
"properties": {
|
|
27
|
-
"slug": "a-produce-shared-value",
|
|
28
|
-
"title": "Produce the shared value B and C both need",
|
|
29
|
-
"type": "chore",
|
|
30
|
-
"contract": { "input": {}, "output": { "value": "<integer>" } },
|
|
31
|
-
"acceptance": ["writes a JSON object with a single integer field 'value' to its checkpoint output"],
|
|
32
|
-
"verify": null,
|
|
33
|
-
"failure_policy": "retry",
|
|
34
|
-
"model_tier": "cheap"
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"id": 5,
|
|
39
|
-
"labels": ["GbuildNode", "Code"],
|
|
40
|
-
"properties": {
|
|
41
|
-
"slug": "b-consume-doubled",
|
|
42
|
-
"title": "Consume the shared value, produce its double",
|
|
43
|
-
"type": "code",
|
|
44
|
-
"contract": {
|
|
45
|
-
"input": { "value": "<integer, from a-produce-shared-value.output.value>" },
|
|
46
|
-
"output": { "doubled": "<integer>" }
|
|
47
|
-
},
|
|
48
|
-
"acceptance": ["output.doubled equals a's output.value multiplied by 2"],
|
|
49
|
-
"verify": null,
|
|
50
|
-
"failure_policy": "repair",
|
|
51
|
-
"model_tier": "cheap"
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"id": 6,
|
|
56
|
-
"labels": ["GbuildNode", "Code"],
|
|
57
|
-
"properties": {
|
|
58
|
-
"slug": "c-consume-squared",
|
|
59
|
-
"title": "Consume the shared value, produce its square",
|
|
60
|
-
"type": "code",
|
|
61
|
-
"contract": {
|
|
62
|
-
"input": { "value": "<integer, from a-produce-shared-value.output.value>" },
|
|
63
|
-
"output": { "squared": "<integer>" }
|
|
64
|
-
},
|
|
65
|
-
"acceptance": ["output.squared equals a's output.value raised to the power of 2"],
|
|
66
|
-
"verify": null,
|
|
67
|
-
"failure_policy": "repair",
|
|
68
|
-
"model_tier": "cheap"
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
"id": 7,
|
|
73
|
-
"labels": ["GbuildNode", "Code"],
|
|
74
|
-
"properties": {
|
|
75
|
-
"slug": "d-join-and-sum",
|
|
76
|
-
"title": "Join B and C, produce their sum",
|
|
77
|
-
"type": "code",
|
|
78
|
-
"contract": {
|
|
79
|
-
"input": {
|
|
80
|
-
"doubled": "<integer, from b-consume-doubled.output.doubled>",
|
|
81
|
-
"squared": "<integer, from c-consume-squared.output.squared>"
|
|
82
|
-
},
|
|
83
|
-
"output": { "sum": "<integer>" }
|
|
84
|
-
},
|
|
85
|
-
"acceptance": [
|
|
86
|
-
"output.sum equals b's output.doubled plus c's output.squared",
|
|
87
|
-
"only runs after both b-consume-doubled and c-consume-squared are checkpointed completed"
|
|
88
|
-
],
|
|
89
|
-
"verify": null,
|
|
90
|
-
"failure_policy": "repair",
|
|
91
|
-
"model_tier": "cheap"
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
],
|
|
95
|
-
"relationships": [
|
|
96
|
-
{ "id": 1, "type": "HAS_ACCEPTANCE", "start_node": 1, "end_node": 2, "properties": {} },
|
|
97
|
-
{ "id": 2, "type": "HAS_ACCEPTANCE", "start_node": 1, "end_node": 3, "properties": {} },
|
|
98
|
-
{ "id": 3, "type": "SATISFIES", "start_node": 5, "end_node": 2, "properties": {} },
|
|
99
|
-
{ "id": 4, "type": "SATISFIES", "start_node": 6, "end_node": 2, "properties": {} },
|
|
100
|
-
{ "id": 5, "type": "SATISFIES", "start_node": 7, "end_node": 3, "properties": {} },
|
|
101
|
-
{ "id": 6, "type": "DEPENDS_ON", "start_node": 5, "end_node": 4, "properties": {} },
|
|
102
|
-
{ "id": 7, "type": "DEPENDS_ON", "start_node": 6, "end_node": 4, "properties": {} },
|
|
103
|
-
{ "id": 8, "type": "DEPENDS_ON", "start_node": 7, "end_node": 5, "properties": {} },
|
|
104
|
-
{ "id": 9, "type": "DEPENDS_ON", "start_node": 7, "end_node": 6, "properties": {} }
|
|
105
|
-
],
|
|
106
|
-
"indexes": [],
|
|
107
|
-
"constraints": [],
|
|
108
|
-
"next_node_id": 8,
|
|
109
|
-
"next_rel_id": 10
|
|
110
|
-
}
|