@caddis/cli 0.4.52 → 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.
- package/bundles/antigravity-plugin/CADDIS-GLOSSARY.md +1 -0
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/repo_freshness.py +132 -5
- package/bundles/antigravity-plugin/scripts/caddis_checkout_guard.py +223 -0
- package/bundles/antigravity-plugin/scripts/caddis_docs_pr.py +284 -0
- package/bundles/antigravity-plugin/scripts/caddis_exit.py +2 -0
- package/bundles/antigravity-plugin/scripts/caddis_frontmatter.py +1 -0
- package/bundles/antigravity-plugin/scripts/caddis_gate.py +226 -195
- package/bundles/antigravity-plugin/scripts/caddis_glossary.py +3 -3
- package/bundles/antigravity-plugin/scripts/caddis_governor.py +48 -4
- package/bundles/antigravity-plugin/scripts/caddis_inventory.py +38 -0
- package/bundles/antigravity-plugin/scripts/caddis_lanes.py +294 -127
- package/bundles/antigravity-plugin/scripts/caddis_lineage_publish.py +458 -0
- package/bundles/antigravity-plugin/scripts/caddis_merge_chain.py +237 -0
- package/bundles/antigravity-plugin/scripts/caddis_notify.py +121 -0
- package/bundles/antigravity-plugin/scripts/caddis_prod_check.py +380 -0
- package/bundles/antigravity-plugin/scripts/caddis_redact.py +99 -0
- package/bundles/antigravity-plugin/scripts/caddis_relay_git.py +2 -2
- package/bundles/antigravity-plugin/scripts/caddis_review.py +1309 -0
- package/bundles/antigravity-plugin/scripts/caddis_roadmap.py +2056 -0
- package/bundles/antigravity-plugin/scripts/caddis_roadmap_run.py +2572 -0
- package/bundles/antigravity-plugin/scripts/caddis_routing.py +3 -2
- package/bundles/antigravity-plugin/scripts/caddis_rows.py +476 -0
- package/bundles/antigravity-plugin/scripts/caddis_spawn.py +8 -9
- package/bundles/antigravity-plugin/scripts/caddis_spend.py +415 -0
- package/bundles/antigravity-plugin/scripts/caddis_tidy.py +357 -20
- package/bundles/antigravity-plugin/scripts/caddis_todo.py +55 -0
- package/bundles/antigravity-plugin/scripts/caddis_workstreams.py +178 -19
- package/bundles/antigravity-plugin/scripts/caddis_worktree_close.py +349 -0
- package/bundles/antigravity-plugin/scripts/check_doc_coverage.py +0 -56
- package/bundles/antigravity-plugin/scripts/claudster_doctor.py +1 -1
- package/bundles/antigravity-plugin/scripts/enforcement_inventory.py +0 -1
- package/bundles/antigravity-plugin/scripts/oss_review.py +2 -2
- package/bundles/antigravity-plugin/scripts/roadmap-recheck.ps1 +89 -0
- package/bundles/antigravity-plugin/scripts/roadmap-run.ps1 +88 -0
- package/bundles/antigravity-plugin/scripts/setup_project_ai.py +43 -1
- package/bundles/antigravity-plugin/session_end_agy.py +3 -3
- package/bundles/antigravity-plugin/skills/_registry.md +2 -6
- package/bundles/antigravity-plugin/skills/best-practices/agents/best-practices-referencer.md +1 -1
- package/bundles/antigravity-plugin/skills/best-practices/agents/codebase-context-builder.md +1 -1
- package/bundles/antigravity-plugin/skills/best-practices/agents/task-intent-analyzer.md +1 -1
- package/bundles/antigravity-plugin/skills/catchup/SKILL.md +7 -0
- package/bundles/antigravity-plugin/skills/cloud-phases/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +5 -2
- package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +29 -83
- package/bundles/antigravity-plugin/skills/handoff/SKILL.md +77 -48
- package/bundles/antigravity-plugin/skills/implement/SKILL.md +87 -147
- package/bundles/antigravity-plugin/skills/migrate-dir/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/mmr/SKILL.md +5 -2
- package/bundles/antigravity-plugin/skills/office-documents-official/SKILL.md +24 -0
- package/bundles/antigravity-plugin/skills/park/SKILL.md +0 -1
- package/bundles/antigravity-plugin/skills/pr-merge/SKILL.md +8 -14
- package/bundles/antigravity-plugin/skills/prd/SKILL.md +2 -10
- package/bundles/antigravity-plugin/skills/roadmap/SKILL.md +259 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/plan-resume-prompt.md +77 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/render-check.template.mjs +68 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/review-protocol.md +78 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/roadmap.template.md +32 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/value-check.template.py +161 -0
- package/bundles/antigravity-plugin/skills/ship/SKILL.md +14 -14
- package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +6 -0
- package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +33 -14
- package/bundles/antigravity-plugin/skills/spawn-session/SKILL.md +4 -8
- package/bundles/antigravity-plugin/warm_start_agy.py +1 -3
- package/bundles/antigravity-plugin-extras/plugin.json +1 -1
- package/bundles/antigravity-plugin-extras/skills/_registry.md +1 -2
- package/bundles/antigravity-plugin-extras/skills/agent-orchestration/SKILL.md +5 -5
- package/bundles/antigravity-plugin-extras/skills/cross-review/SKILL.md +5 -2
- package/bundles/antigravity-plugin-extras/skills/runner-move/SKILL.md +214 -0
- package/bundles/codex/.codex/skills/docs/office-documents-official/SKILL.md +24 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/agents/best-practices-referencer.md +1 -1
- package/bundles/codex/.codex/skills/workflow/best-practices/agents/codebase-context-builder.md +1 -1
- package/bundles/codex/.codex/skills/workflow/best-practices/agents/task-intent-analyzer.md +1 -1
- package/bundles/codex/.codex/skills/workflow/roadmap/SKILL.md +259 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/plan-resume-prompt.md +77 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/render-check.template.mjs +68 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/review-protocol.md +78 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/roadmap.template.md +32 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/value-check.template.py +161 -0
- package/bundles/codex/AGENTS.md +5 -4
- package/bundles/manifest.json +3 -3
- package/package.json +1 -1
- package/bundles/antigravity-plugin/skills/advisory-hub/SKILL.md +0 -233
- package/bundles/antigravity-plugin/skills/advisory-hub/reference/context-template.md +0 -214
- package/bundles/antigravity-plugin/skills/advisory-hub/reference/hub-handbook.md +0 -233
- package/bundles/antigravity-plugin/skills/advisory-hub/reference/report-contract.md +0 -94
- package/bundles/antigravity-plugin/skills/advisory-hub/reference/succession.md +0 -118
- package/bundles/antigravity-plugin/skills/resume/SKILL.md +0 -6
- package/bundles/antigravity-plugin/skills/review/SKILL.md +0 -6
- package/bundles/antigravity-plugin/skills/spawn-hub/SKILL.md +0 -341
- package/bundles/antigravity-plugin/skills/statusline/SKILL.md +0 -6
- package/bundles/antigravity-plugin/skills/validate-phase/SKILL.md +0 -143
- package/bundles/antigravity-plugin-extras/skills/data-lineage/SKILL.md +0 -158
- package/bundles/antigravity-plugin-extras/skills/data-lineage/scripts/convert_doc_to_confluence.py +0 -1430
- package/bundles/antigravity-plugin-extras/skills/golden-plan/SKILL.md +0 -586
- package/bundles/codex/.codex/skills/workflow/advisory-hub/SKILL.md +0 -233
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/context-template.md +0 -214
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/hub-handbook.md +0 -233
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/report-contract.md +0 -94
- 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())
|