@caddis/cli 0.4.51 → 0.4.53

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.
Files changed (100) hide show
  1. package/bundles/antigravity-plugin/CADDIS-GLOSSARY.md +1 -0
  2. package/bundles/antigravity-plugin/plugin.json +1 -1
  3. package/bundles/antigravity-plugin/repo_freshness.py +132 -5
  4. package/bundles/antigravity-plugin/scripts/caddis_checkout_guard.py +223 -0
  5. package/bundles/antigravity-plugin/scripts/caddis_docs_pr.py +284 -0
  6. package/bundles/antigravity-plugin/scripts/caddis_exit.py +2 -0
  7. package/bundles/antigravity-plugin/scripts/caddis_frontmatter.py +1 -0
  8. package/bundles/antigravity-plugin/scripts/caddis_gate.py +226 -195
  9. package/bundles/antigravity-plugin/scripts/caddis_glossary.py +3 -3
  10. package/bundles/antigravity-plugin/scripts/caddis_governor.py +48 -4
  11. package/bundles/antigravity-plugin/scripts/caddis_inventory.py +38 -0
  12. package/bundles/antigravity-plugin/scripts/caddis_lanes.py +294 -127
  13. package/bundles/antigravity-plugin/scripts/caddis_lineage_publish.py +458 -0
  14. package/bundles/antigravity-plugin/scripts/caddis_merge_chain.py +237 -0
  15. package/bundles/antigravity-plugin/scripts/caddis_notify.py +121 -0
  16. package/bundles/antigravity-plugin/scripts/caddis_prod_check.py +380 -0
  17. package/bundles/antigravity-plugin/scripts/caddis_redact.py +99 -0
  18. package/bundles/antigravity-plugin/scripts/caddis_relay_git.py +2 -2
  19. package/bundles/antigravity-plugin/scripts/caddis_review.py +1309 -0
  20. package/bundles/antigravity-plugin/scripts/caddis_roadmap.py +2056 -0
  21. package/bundles/antigravity-plugin/scripts/caddis_roadmap_run.py +2572 -0
  22. package/bundles/antigravity-plugin/scripts/caddis_routing.py +3 -2
  23. package/bundles/antigravity-plugin/scripts/caddis_rows.py +476 -0
  24. package/bundles/antigravity-plugin/scripts/caddis_spawn.py +8 -9
  25. package/bundles/antigravity-plugin/scripts/caddis_spend.py +415 -0
  26. package/bundles/antigravity-plugin/scripts/caddis_tidy.py +357 -20
  27. package/bundles/antigravity-plugin/scripts/caddis_todo.py +55 -0
  28. package/bundles/antigravity-plugin/scripts/caddis_workstreams.py +178 -19
  29. package/bundles/antigravity-plugin/scripts/caddis_worktree_close.py +349 -0
  30. package/bundles/antigravity-plugin/scripts/check_doc_coverage.py +0 -56
  31. package/bundles/antigravity-plugin/scripts/claudster_doctor.py +1 -1
  32. package/bundles/antigravity-plugin/scripts/enforcement_inventory.py +0 -1
  33. package/bundles/antigravity-plugin/scripts/oss_review.py +2 -2
  34. package/bundles/antigravity-plugin/scripts/roadmap-recheck.ps1 +89 -0
  35. package/bundles/antigravity-plugin/scripts/roadmap-run.ps1 +88 -0
  36. package/bundles/antigravity-plugin/scripts/setup_project_ai.py +86 -2
  37. package/bundles/antigravity-plugin/session_end_agy.py +3 -3
  38. package/bundles/antigravity-plugin/skills/_registry.md +2 -6
  39. package/bundles/antigravity-plugin/skills/best-practices/agents/best-practices-referencer.md +1 -1
  40. package/bundles/antigravity-plugin/skills/best-practices/agents/codebase-context-builder.md +1 -1
  41. package/bundles/antigravity-plugin/skills/best-practices/agents/task-intent-analyzer.md +1 -1
  42. package/bundles/antigravity-plugin/skills/catchup/SKILL.md +7 -0
  43. package/bundles/antigravity-plugin/skills/cloud-phases/SKILL.md +1 -1
  44. package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +5 -2
  45. package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +29 -83
  46. package/bundles/antigravity-plugin/skills/handoff/SKILL.md +77 -48
  47. package/bundles/antigravity-plugin/skills/implement/SKILL.md +87 -147
  48. package/bundles/antigravity-plugin/skills/migrate-dir/SKILL.md +1 -1
  49. package/bundles/antigravity-plugin/skills/mmr/SKILL.md +5 -2
  50. package/bundles/antigravity-plugin/skills/office-documents-official/SKILL.md +24 -0
  51. package/bundles/antigravity-plugin/skills/park/SKILL.md +0 -1
  52. package/bundles/antigravity-plugin/skills/pr-merge/SKILL.md +8 -14
  53. package/bundles/antigravity-plugin/skills/prd/SKILL.md +2 -10
  54. package/bundles/antigravity-plugin/skills/roadmap/SKILL.md +259 -0
  55. package/bundles/antigravity-plugin/skills/roadmap/assets/plan-resume-prompt.md +77 -0
  56. package/bundles/antigravity-plugin/skills/roadmap/assets/render-check.template.mjs +68 -0
  57. package/bundles/antigravity-plugin/skills/roadmap/assets/review-protocol.md +78 -0
  58. package/bundles/antigravity-plugin/skills/roadmap/assets/roadmap.template.md +32 -0
  59. package/bundles/antigravity-plugin/skills/roadmap/assets/value-check.template.py +161 -0
  60. package/bundles/antigravity-plugin/skills/ship/SKILL.md +14 -14
  61. package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +6 -0
  62. package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +33 -14
  63. package/bundles/antigravity-plugin/skills/spawn-session/SKILL.md +4 -8
  64. package/bundles/antigravity-plugin/warm_start_agy.py +1 -3
  65. package/bundles/antigravity-plugin-extras/plugin.json +1 -1
  66. package/bundles/antigravity-plugin-extras/skills/_registry.md +1 -2
  67. package/bundles/antigravity-plugin-extras/skills/agent-orchestration/SKILL.md +5 -5
  68. package/bundles/antigravity-plugin-extras/skills/cross-review/SKILL.md +5 -2
  69. package/bundles/antigravity-plugin-extras/skills/runner-move/SKILL.md +214 -0
  70. package/bundles/codex/.codex/skills/docs/office-documents-official/SKILL.md +24 -0
  71. package/bundles/codex/.codex/skills/workflow/best-practices/agents/best-practices-referencer.md +1 -1
  72. package/bundles/codex/.codex/skills/workflow/best-practices/agents/codebase-context-builder.md +1 -1
  73. package/bundles/codex/.codex/skills/workflow/best-practices/agents/task-intent-analyzer.md +1 -1
  74. package/bundles/codex/.codex/skills/workflow/roadmap/SKILL.md +259 -0
  75. package/bundles/codex/.codex/skills/workflow/roadmap/assets/plan-resume-prompt.md +77 -0
  76. package/bundles/codex/.codex/skills/workflow/roadmap/assets/render-check.template.mjs +68 -0
  77. package/bundles/codex/.codex/skills/workflow/roadmap/assets/review-protocol.md +78 -0
  78. package/bundles/codex/.codex/skills/workflow/roadmap/assets/roadmap.template.md +32 -0
  79. package/bundles/codex/.codex/skills/workflow/roadmap/assets/value-check.template.py +161 -0
  80. package/bundles/codex/AGENTS.md +5 -4
  81. package/bundles/manifest.json +3 -3
  82. package/package.json +1 -1
  83. package/bundles/antigravity-plugin/skills/advisory-hub/SKILL.md +0 -233
  84. package/bundles/antigravity-plugin/skills/advisory-hub/reference/context-template.md +0 -214
  85. package/bundles/antigravity-plugin/skills/advisory-hub/reference/hub-handbook.md +0 -233
  86. package/bundles/antigravity-plugin/skills/advisory-hub/reference/report-contract.md +0 -94
  87. package/bundles/antigravity-plugin/skills/advisory-hub/reference/succession.md +0 -118
  88. package/bundles/antigravity-plugin/skills/resume/SKILL.md +0 -6
  89. package/bundles/antigravity-plugin/skills/review/SKILL.md +0 -6
  90. package/bundles/antigravity-plugin/skills/spawn-hub/SKILL.md +0 -341
  91. package/bundles/antigravity-plugin/skills/statusline/SKILL.md +0 -6
  92. package/bundles/antigravity-plugin/skills/validate-phase/SKILL.md +0 -143
  93. package/bundles/antigravity-plugin-extras/skills/data-lineage/SKILL.md +0 -158
  94. package/bundles/antigravity-plugin-extras/skills/data-lineage/scripts/convert_doc_to_confluence.py +0 -1430
  95. package/bundles/antigravity-plugin-extras/skills/golden-plan/SKILL.md +0 -586
  96. package/bundles/codex/.codex/skills/workflow/advisory-hub/SKILL.md +0 -233
  97. package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/context-template.md +0 -214
  98. package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/hub-handbook.md +0 -233
  99. package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/report-contract.md +0 -94
  100. package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/succession.md +0 -118
@@ -0,0 +1,458 @@
1
+ """Programme lineage step: check, publish and freshness of an app's data lineage document.
2
+
3
+ `check` (part 13a): every table of the programme contract is in section 3 (the matrix) of the lineage document, and the
4
+ section 5 graph version is higher than at the base. `publish` (part 13c): copy the document to the docs folder the lineage
5
+ monitor reads (`--hla-dir` or HLA_DOCS_DIR), run the monitor's importer (`import_lineage.py` in its scripts folder) on the copy, and read the
6
+ monitor's snapshot back (its `as_of` equals the new version and every contract table is imported). `freshness` (part
7
+ 13d): every contract table has a freshness signal, `source_coverage <key>` or `probe <name>`.
8
+ Exit codes: 0 clean, 1 a check failed, 3 a folder is not configured, 4 bad input.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ from dataclasses import dataclass
15
+ import json
16
+ import os
17
+ from pathlib import Path
18
+ import re
19
+ import subprocess
20
+ import sys
21
+
22
+ try:
23
+ import caddis_exit
24
+ except ModuleNotFoundError as exc:
25
+ if exc.name != "caddis_exit":
26
+ raise
27
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
28
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "claude-harness" / "scripts"))
29
+ import caddis_exit
30
+
31
+
32
+ class ContractParseError(Exception):
33
+ """Raised when the contract markdown file cannot be read or parsed."""
34
+
35
+
36
+ @dataclass
37
+ class ContractTable:
38
+ name: str
39
+ freshness_signal: str | None
40
+
41
+
42
+ def parse_contract_tables(contract_path: Path) -> tuple[list[ContractTable], bool]:
43
+ """Parse contract tables from a contract markdown file.
44
+
45
+ Returns a list of ContractTable objects and a boolean indicating whether the
46
+ 'Freshness signal' column was present in the header.
47
+ """
48
+ try:
49
+ text = contract_path.read_text(encoding="utf-8")
50
+ except Exception as exc:
51
+ print(f"failed to read contract file {contract_path}: {exc}", file=sys.stderr)
52
+ raise ContractParseError() from exc
53
+
54
+ lines = text.splitlines()
55
+ in_section = False
56
+ section_lines: list[str] = []
57
+ for line in lines:
58
+ stripped = line.strip()
59
+ if re.match(r"^##\s+Tables\b", stripped):
60
+ in_section = True
61
+ continue
62
+ if in_section and stripped.startswith("## "):
63
+ break
64
+ if in_section:
65
+ section_lines.append(line)
66
+
67
+ if not in_section:
68
+ print(f"no '## Tables' section found in contract {contract_path}", file=sys.stderr)
69
+ raise ContractParseError()
70
+
71
+ header_idx = -1
72
+ table_col_idx = -1
73
+ freshness_col_idx = -1
74
+ for idx, line in enumerate(section_lines):
75
+ line_s = line.strip()
76
+ if line_s.startswith("|"):
77
+ cells = [c.strip() for c in line_s.strip("|").split("|")]
78
+ for c_idx, cell in enumerate(cells):
79
+ if cell == "Table":
80
+ table_col_idx = c_idx
81
+ elif cell == "Freshness signal":
82
+ freshness_col_idx = c_idx
83
+ if table_col_idx != -1:
84
+ header_idx = idx
85
+ break
86
+
87
+ if header_idx == -1 or table_col_idx == -1:
88
+ print(f"no header row with 'Table' column in contract {contract_path}", file=sys.stderr)
89
+ raise ContractParseError()
90
+
91
+ has_freshness_col = freshness_col_idx != -1
92
+ tables: list[ContractTable] = []
93
+ for line in section_lines[header_idx + 1:]:
94
+ line_s = line.strip()
95
+ if not line_s.startswith("|"):
96
+ continue
97
+ raw_cells = [c.strip() for c in line_s.strip("|").split("|")]
98
+ if all(re.match(r"^:?-+:?$", c) for c in raw_cells if c):
99
+ continue
100
+ if table_col_idx < len(raw_cells):
101
+ tbl_cell = raw_cells[table_col_idx].strip("`").strip()
102
+ if not tbl_cell:
103
+ continue
104
+ sig_val: str | None = None
105
+ if has_freshness_col:
106
+ if freshness_col_idx < len(raw_cells):
107
+ sig_val = raw_cells[freshness_col_idx]
108
+ else:
109
+ sig_val = ""
110
+ tables.append(ContractTable(name=tbl_cell, freshness_signal=sig_val))
111
+
112
+ if not tables:
113
+ print(f"no data rows found in '## Tables' section of {contract_path}", file=sys.stderr)
114
+ raise ContractParseError()
115
+
116
+ return tables, has_freshness_col
117
+
118
+
119
+ def get_section_3_rows(doc_text: str) -> list[str]:
120
+ """Return lines starting with '|' between heading matching '^## 3\\.' and next '^## '."""
121
+ lines = doc_text.splitlines()
122
+ in_section_3 = False
123
+ rows: list[str] = []
124
+ for line in lines:
125
+ stripped = line.strip()
126
+ if re.match(r"^## 3\.", stripped):
127
+ in_section_3 = True
128
+ continue
129
+ if in_section_3 and stripped.startswith("## "):
130
+ break
131
+ if in_section_3 and stripped.startswith("|"):
132
+ rows.append(stripped)
133
+ return rows
134
+
135
+
136
+ def is_table_in_section_3(table_name: str, section_3_rows: list[str]) -> bool:
137
+ """Return True if table_name appears as a whole name in any section 3 row."""
138
+ pattern = re.compile(r"(?<![\w.])" + re.escape(table_name) + r"(?![\w.])")
139
+ return any(pattern.search(row) for row in section_3_rows)
140
+
141
+
142
+ def extract_section_5_version(doc_text: str) -> str | None:
143
+ """Return the first version string after heading matching '^## 5\\.'."""
144
+ lines = doc_text.splitlines()
145
+ start_idx = -1
146
+ for idx, line in enumerate(lines):
147
+ if re.match(r"^## 5\.", line.strip()):
148
+ start_idx = idx
149
+ break
150
+ if start_idx == -1:
151
+ return None
152
+ rest = "\n".join(lines[start_idx:])
153
+ m = re.search(r'"version"\s*:\s*"(\d{4}-\d{2}-\d{2})"', rest)
154
+ if m:
155
+ return m.group(1)
156
+ return None
157
+
158
+
159
+ def cmd_check(args: argparse.Namespace) -> int:
160
+ """Run check subcommand."""
161
+ try:
162
+ doc_text = args.doc.read_text(encoding="utf-8")
163
+ except Exception as exc:
164
+ print(f"failed to read doc {args.doc}: {exc}", file=sys.stderr)
165
+ return caddis_exit.ERROR
166
+
167
+ try:
168
+ tables, _ = parse_contract_tables(args.contract)
169
+ except ContractParseError:
170
+ return caddis_exit.ERROR
171
+
172
+ failure = False
173
+ section_3_rows = get_section_3_rows(doc_text)
174
+ for table in tables:
175
+ if not is_table_in_section_3(table.name, section_3_rows):
176
+ print(f"MISSING {table.name}")
177
+ failure = True
178
+
179
+ new_version = extract_section_5_version(doc_text)
180
+ if new_version is None:
181
+ print(f"no section 5 version in {args.doc}")
182
+ failure = True
183
+ else:
184
+ base_version: str | None = None
185
+ is_new_at_ref = False
186
+ if args.base_doc is not None:
187
+ try:
188
+ base_text = args.base_doc.read_text(encoding="utf-8")
189
+ base_version = extract_section_5_version(base_text)
190
+ except Exception as exc:
191
+ print(f"failed to read base doc {args.base_doc}: {exc}", file=sys.stderr)
192
+ return caddis_exit.ERROR
193
+ else:
194
+ doc_path = args.doc.resolve()
195
+ doc_folder = doc_path.parent
196
+ proc_top = subprocess.run(
197
+ ["git", "-C", str(doc_folder), "rev-parse", "--show-toplevel"],
198
+ capture_output=True,
199
+ text=True,
200
+ encoding="utf-8",
201
+ errors="replace",
202
+ )
203
+ if proc_top.returncode != 0:
204
+ print(f"git rev-parse failed for {doc_folder}: {proc_top.stderr.strip()}", file=sys.stderr)
205
+ return caddis_exit.ERROR
206
+ top_dir = Path(proc_top.stdout.strip()).resolve()
207
+ try:
208
+ relpath = doc_path.relative_to(top_dir).as_posix()
209
+ except ValueError:
210
+ print(f"doc {doc_path} is not under git toplevel {top_dir}", file=sys.stderr)
211
+ return caddis_exit.ERROR
212
+
213
+ proc_show = subprocess.run(
214
+ ["git", "-C", str(top_dir), "show", f"{args.base_ref}:{relpath}"],
215
+ capture_output=True,
216
+ text=True,
217
+ encoding="utf-8",
218
+ errors="replace",
219
+ )
220
+ if proc_show.returncode != 0:
221
+ base_version = None
222
+ is_new_at_ref = True
223
+ else:
224
+ base_version = extract_section_5_version(proc_show.stdout)
225
+
226
+ if base_version is None:
227
+ if is_new_at_ref:
228
+ print(f"version {new_version} (new at {args.base_ref})")
229
+ elif args.base_doc is not None:
230
+ print(f"version {new_version} (new at {args.base_doc})")
231
+ else:
232
+ print(f"version {new_version}")
233
+ else:
234
+ if new_version <= base_version:
235
+ print(f"version not raised: {base_version} -> {new_version}")
236
+ failure = True
237
+ else:
238
+ print(f"version {base_version} -> {new_version}")
239
+
240
+ if failure:
241
+ return caddis_exit.BLOCKED
242
+
243
+ print(f"check ok: {len(tables)} tables in section 3")
244
+ return caddis_exit.CLEAN
245
+
246
+
247
+ def cmd_freshness(args: argparse.Namespace) -> int:
248
+ """Run freshness subcommand."""
249
+ try:
250
+ tables, has_freshness_col = parse_contract_tables(args.contract)
251
+ except ContractParseError:
252
+ return caddis_exit.ERROR
253
+
254
+ if not has_freshness_col:
255
+ print(f"contract {args.contract} is missing 'Freshness signal' column", file=sys.stderr)
256
+ return caddis_exit.ERROR
257
+
258
+ failure = False
259
+ for table in tables:
260
+ sig = (table.freshness_signal or "").replace("`", "").strip()
261
+ if not re.match(r"^(?:source_coverage|probe) \S+$", sig):
262
+ print(f"NO FRESHNESS {table.name}")
263
+ failure = True
264
+
265
+ if failure:
266
+ return caddis_exit.BLOCKED
267
+
268
+ print(f"freshness ok: {len(tables)} tables")
269
+ return caddis_exit.CLEAN
270
+
271
+
272
+ def cmd_publish(args: argparse.Namespace) -> int:
273
+ """Run publish subcommand."""
274
+ hla_dir_val = args.hla_dir if args.hla_dir else os.environ.get("HLA_DOCS_DIR")
275
+ if not hla_dir_val:
276
+ print("HLA_DOCS_DIR is unset or not provided", file=sys.stderr)
277
+ return caddis_exit.NOT_RUN
278
+
279
+ hla_path = Path(hla_dir_val)
280
+ if not hla_path.is_dir():
281
+ print(f"HLA_DOCS_DIR '{hla_dir_val}' is not an existing directory", file=sys.stderr)
282
+ return caddis_exit.NOT_RUN
283
+
284
+ monitor_val = args.monitor_dir if args.monitor_dir else os.environ.get("LINEAGE_MONITOR_DIR")
285
+ if not monitor_val:
286
+ print("LINEAGE_MONITOR_DIR is unset or not provided", file=sys.stderr)
287
+ return caddis_exit.NOT_RUN
288
+
289
+ ws_path = Path(monitor_val)
290
+ if not ws_path.is_dir():
291
+ print(f"LINEAGE_MONITOR_DIR '{monitor_val}' is not an existing directory", file=sys.stderr)
292
+ return caddis_exit.NOT_RUN
293
+
294
+ importer_script = ws_path / "scripts" / "import_lineage.py"
295
+ if not importer_script.is_file():
296
+ print(f"the lineage monitor importer script not found at {importer_script}", file=sys.stderr)
297
+ return caddis_exit.NOT_RUN
298
+
299
+ try:
300
+ doc_text = args.doc.read_text(encoding="utf-8")
301
+ except Exception as exc:
302
+ print(f"failed to read doc {args.doc}: {exc}", file=sys.stderr)
303
+ return caddis_exit.ERROR
304
+
305
+ version = extract_section_5_version(doc_text)
306
+ if version is None:
307
+ print(f"no section 5 version in {args.doc}")
308
+ return caddis_exit.BLOCKED
309
+
310
+ try:
311
+ tables, _ = parse_contract_tables(args.contract)
312
+ except ContractParseError:
313
+ return caddis_exit.ERROR
314
+
315
+ app_dir = hla_path / args.app
316
+ app_dir.mkdir(parents=True, exist_ok=True)
317
+ copied = app_dir / f"{args.app}-data-lineage.md"
318
+ try:
319
+ doc_bytes = args.doc.read_bytes()
320
+ copied.write_bytes(doc_bytes)
321
+ read_bytes = copied.read_bytes()
322
+ if read_bytes != doc_bytes:
323
+ print(f"copy failed: bytes differ for {copied}", file=sys.stderr)
324
+ return caddis_exit.ERROR
325
+ except Exception as exc:
326
+ print(f"failed to copy doc to {copied}: {exc}", file=sys.stderr)
327
+ return caddis_exit.ERROR
328
+
329
+ py_win = ws_path / ".venv" / "Scripts" / "python.exe"
330
+ py_nix = ws_path / ".venv" / "bin" / "python"
331
+ if py_win.is_file():
332
+ python_bin = str(py_win)
333
+ elif py_nix.is_file():
334
+ python_bin = str(py_nix)
335
+ else:
336
+ python_bin = sys.executable
337
+
338
+ cmd = [python_bin, str(importer_script), str(copied), "--app", args.app]
339
+ try:
340
+ proc = subprocess.run(
341
+ cmd,
342
+ cwd=str(ws_path),
343
+ capture_output=True,
344
+ text=True,
345
+ encoding="utf-8",
346
+ errors="replace",
347
+ timeout=300,
348
+ )
349
+ except subprocess.TimeoutExpired:
350
+ print("the lineage monitor importer timed out after 300 s", file=sys.stderr)
351
+ return caddis_exit.BLOCKED
352
+ except Exception as exc:
353
+ print(f"failed to run the lineage monitor importer: {exc}", file=sys.stderr)
354
+ return caddis_exit.ERROR
355
+
356
+ if proc.stdout:
357
+ sys.stdout.write(proc.stdout)
358
+ if not proc.stdout.endswith("\n"):
359
+ sys.stdout.write("\n")
360
+
361
+ if proc.returncode != 0:
362
+ if proc.stderr:
363
+ sys.stderr.write(proc.stderr)
364
+ if not proc.stderr.endswith("\n"):
365
+ sys.stderr.write("\n")
366
+ return caddis_exit.BLOCKED
367
+
368
+ # The monitor writes `config/lineage-snapshot-<app>.json`, except for its first app, whose snapshot is the
369
+ # unsuffixed file; that file counts only when its `app` field names this app.
370
+ snapshot_data = None
371
+ for snapshot_path in (ws_path / "config" / f"lineage-snapshot-{args.app}.json",
372
+ ws_path / "config" / "lineage-snapshot.json"):
373
+ if not snapshot_path.is_file():
374
+ continue
375
+ try:
376
+ candidate = json.loads(snapshot_path.read_text(encoding="utf-8"))
377
+ except (OSError, ValueError) as exc:
378
+ print(f"the lineage monitor snapshot {snapshot_path} is not valid JSON: {exc}")
379
+ return caddis_exit.BLOCKED
380
+ if isinstance(candidate, dict) and candidate.get("app") == args.app:
381
+ snapshot_data = candidate
382
+ break
383
+ if snapshot_data is None:
384
+ print(f"no lineage monitor snapshot for app {args.app} under {ws_path / 'config'}")
385
+ return caddis_exit.BLOCKED
386
+
387
+ snapshot_as_of = snapshot_data.get("as_of")
388
+ if snapshot_as_of != version:
389
+ print(f"snapshot as_of {snapshot_as_of}, expected {version}")
390
+ return caddis_exit.BLOCKED
391
+
392
+ imported_names: set[str] = set()
393
+ elements = snapshot_data.get("elements")
394
+ if isinstance(elements, list):
395
+ for elem in elements:
396
+ if isinstance(elem, dict):
397
+ datasets = elem.get("datasets")
398
+ if isinstance(datasets, list):
399
+ for ds in datasets:
400
+ if isinstance(ds, dict) and "name" in ds:
401
+ imported_names.add(ds["name"])
402
+
403
+ missing_from_snapshot = False
404
+ for table in tables:
405
+ if table.name not in imported_names:
406
+ print(f"NOT IMPORTED {table.name}")
407
+ missing_from_snapshot = True
408
+
409
+ if missing_from_snapshot:
410
+ return caddis_exit.BLOCKED
411
+
412
+ print(f"published {args.app} version {version}: {copied}; {len(tables)} tables imported")
413
+ return caddis_exit.CLEAN
414
+
415
+
416
+ def main(argv: list[str] | None = None) -> int:
417
+ """CLI entry point for caddis_lineage_publish."""
418
+ if argv is None:
419
+ argv = sys.argv[1:]
420
+
421
+ parser = argparse.ArgumentParser(description="Publish and check data lineage documentation.")
422
+ subparsers = parser.add_subparsers(dest="subcommand", required=True)
423
+
424
+ # Subcommand: check
425
+ p_check = subparsers.add_parser("check", help="Check lineage doc against contract tables and version.")
426
+ p_check.add_argument("--app", required=True, help="Application name")
427
+ p_check.add_argument("--doc", required=True, type=Path, help="Path to lineage doc")
428
+ p_check.add_argument("--contract", required=True, type=Path, help="Path to contract markdown")
429
+ p_check.add_argument("--base-doc", type=Path, default=None, help="Path to base doc for version comparison")
430
+ p_check.add_argument("--base-ref", default="origin/main", help="Git ref for base doc (default: origin/main)")
431
+
432
+ # Subcommand: freshness
433
+ p_freshness = subparsers.add_parser("freshness", help="Verify freshness signals for contract tables.")
434
+ p_freshness.add_argument("--app", required=True, help="Application name")
435
+ p_freshness.add_argument("--contract", required=True, type=Path, help="Path to contract markdown")
436
+
437
+ # Subcommand: publish
438
+ p_publish = subparsers.add_parser("publish", help="Publish lineage doc to HLA docs and trigger import.")
439
+ p_publish.add_argument("--app", required=True, help="Application name")
440
+ p_publish.add_argument("--doc", required=True, type=Path, help="Path to lineage doc")
441
+ p_publish.add_argument("--contract", required=True, type=Path, help="Path to contract markdown")
442
+ p_publish.add_argument("--hla-dir", type=Path, default=None, help="Path to HLA docs directory")
443
+ p_publish.add_argument("--monitor-dir", type=Path, default=None, help="lineage monitor repository (default: env LINEAGE_MONITOR_DIR)")
444
+
445
+ args = parser.parse_args(argv)
446
+
447
+ if args.subcommand == "check":
448
+ return cmd_check(args)
449
+ if args.subcommand == "freshness":
450
+ return cmd_freshness(args)
451
+ if args.subcommand == "publish":
452
+ return cmd_publish(args)
453
+
454
+ return caddis_exit.ERROR
455
+
456
+
457
+ if __name__ == "__main__":
458
+ sys.exit(main())