|
10 | 10 | # or any done/*.yaml). Genuinely dangling: a typo, or an item deleted |
11 | 11 | # instead of archived. Nothing can ever resolve it. |
12 | 12 | # |
| 13 | +# ERROR — the document is not a mapping carrying `items:`. The daemon's shell |
| 14 | +# helpers query the backlog with `yq '.items[] | ...'`; a bare |
| 15 | +# top-level sequence makes every one of them fail. PyYAML is happy |
| 16 | +# either way, so a checker written in Python will not notice — which |
| 17 | +# is exactly how this shipped: a repair script rebuilt the file from |
| 18 | +# its rows and silently dropped the `cutover_state:` / `items:` |
| 19 | +# header, and the recovery scan went blind for hours while reporting |
| 20 | +# that it had nothing to evaluate. |
| 21 | +# |
13 | 22 | # ERROR — a row whose `artefact` filename names a DIFFERENT row. Editing a |
14 | 23 | # backlog by hand can displace a row's tail onto its neighbour: the |
15 | 24 | # YAML stays valid, so nothing complains, while a row now carries |
@@ -65,6 +74,17 @@ def load(path): |
65 | 74 | return doc or [] |
66 | 75 |
|
67 | 76 |
|
| 77 | +def top_level_shape(path): |
| 78 | + """The shape the daemon's yq queries require, not the shape PyYAML tolerates.""" |
| 79 | + with open(path, encoding="utf-8") as fh: |
| 80 | + doc = yaml.safe_load(fh) |
| 81 | + if not isinstance(doc, dict): |
| 82 | + return f"a top-level {type(doc).__name__}, not a mapping" |
| 83 | + if "items" not in doc: |
| 84 | + return "a mapping with no `items:` key" |
| 85 | + return None |
| 86 | +
|
| 87 | +
|
68 | 88 | try: |
69 | 89 | active = load(active_path) |
70 | 90 | except Exception as exc: |
@@ -126,6 +146,19 @@ for item in active: |
126 | 146 | if item.get("status") in (None, ""): |
127 | 147 | statusless.append(item["id"]) |
128 | 148 |
|
| 149 | +shape_problem = top_level_shape(active_path) |
| 150 | +if shape_problem: |
| 151 | + print("", file=sys.stderr) |
| 152 | + print(f"❌ pre-push: {os.path.basename(active_path)} is {shape_problem} — push aborted.", |
| 153 | + file=sys.stderr) |
| 154 | + print(" The daemon queries this file with `yq '.items[] | ...'`, which fails outright", file=sys.stderr) |
| 155 | + print(" on a bare sequence — every shell helper then returns empty, and the recovery", file=sys.stderr) |
| 156 | + print(" scan reports it has nothing to evaluate while stories sit stranded.", file=sys.stderr) |
| 157 | + print(" PyYAML accepts both shapes, so this survives any Python-side check.", file=sys.stderr) |
| 158 | + print(" Fix: restore the `cutover_state:` block and the `items:` key above the rows.", file=sys.stderr) |
| 159 | + print("", file=sys.stderr) |
| 160 | + sys.exit(1) |
| 161 | +
|
129 | 162 | seen, duplicates = set(), set() |
130 | 163 | for item in active: |
131 | 164 | if not isinstance(item, dict) or not isinstance(item.get("id"), str): |
|
0 commit comments