ops-wiki-agent-kit 0.1.1 → 0.1.2
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/package.json +1 -1
- package/.github/agents/docs-target-catalog.agent.md +0 -40
- package/.github/agents/docs-target-queue-from-catalog.agent.md +0 -35
- package/.github/agents/source-code-to-spec-reviewer.agent.md +0 -53
- package/.github/prompts/00-generate-target-all-spec.prompt.md +0 -35
- package/.github/prompts/04-review-target-spec.prompt.md +0 -24
- package/.github/prompts/docs-target-catalog.prompt.md +0 -30
- package/.github/prompts/docs-target-queue-from-catalog.prompt.md +0 -28
- package/.github/prompts/generate-docs-index.prompt.md +0 -77
- package/.github/skills/database-query/SKILL.md +0 -142
- package/.github/skills/database-query/references/client-commands.md +0 -197
- package/.github/skills/database-query/references/query-safety.md +0 -109
- package/.github/skills/database-query/scripts/find_db_config.py +0 -273
- package/.github/skills/docs-target-catalog/SKILL.md +0 -194
- package/.github/skills/docs-target-catalog/references/docs-target-queue-conversion.md +0 -164
- package/.github/skills/docs-target-catalog/references/entrypoint-source-patterns.md +0 -83
- package/.github/skills/docs-target-catalog/references/output-templates.md +0 -168
- package/.github/skills/docs-target-queue-from-catalog/SKILL.md +0 -85
- package/.github/skills/docs-target-queue-from-catalog/references/docs-target-queue-contract.md +0 -172
- package/.github/skills/docs-target-queue-from-catalog/references/metadata-acquisition-patterns.md +0 -244
- package/.github/skills/docs-target-queue-from-catalog/scripts/write_documentation_target_queue.py +0 -544
package/.github/skills/docs-target-queue-from-catalog/scripts/write_documentation_target_queue.py
DELETED
|
@@ -1,544 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
import sys
|
|
3
|
-
|
|
4
|
-
sys.dont_write_bytecode = True
|
|
5
|
-
|
|
6
|
-
import argparse
|
|
7
|
-
import csv
|
|
8
|
-
import json
|
|
9
|
-
import re
|
|
10
|
-
from collections import OrderedDict, defaultdict
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
|
|
13
|
-
for parent in Path(__file__).resolve().parents:
|
|
14
|
-
shared_scripts = parent / "source-code-to-spec-tools" / "scripts"
|
|
15
|
-
if (shared_scripts / "queue_contract.py").exists():
|
|
16
|
-
sys.path.insert(0, str(shared_scripts))
|
|
17
|
-
break
|
|
18
|
-
else:
|
|
19
|
-
raise SystemExit("Cannot locate source-code-to-spec-tools/scripts/queue_contract.py")
|
|
20
|
-
|
|
21
|
-
from queue_contract import (
|
|
22
|
-
BASELINE_STATUSES as VALID_BASELINE_STATUSES,
|
|
23
|
-
DOC_STATUSES as VALID_DOC_STATUSES,
|
|
24
|
-
COVERAGE_COLUMNS,
|
|
25
|
-
DEFAULT_PATH_PATTERNS,
|
|
26
|
-
DEFAULT_PREFIXES,
|
|
27
|
-
MAIN_COLUMNS,
|
|
28
|
-
REVIEW_STATUSES as VALID_REVIEW_STATUSES,
|
|
29
|
-
SUMMARY_COLUMNS,
|
|
30
|
-
normalize_completed_flag,
|
|
31
|
-
normalize_doc_profile,
|
|
32
|
-
normalize_status,
|
|
33
|
-
normalize_text,
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def normalize_path_segment(value):
|
|
38
|
-
value = normalize_text(value)
|
|
39
|
-
if not value:
|
|
40
|
-
return "Unclassified"
|
|
41
|
-
value = re.sub(r'[<>:"/\\|?*]', " ", value)
|
|
42
|
-
value = re.sub(r"\s+", " ", value).strip()
|
|
43
|
-
return value or "Unclassified"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def markdown_escape(value):
|
|
47
|
-
return normalize_text(value).replace("|", "\\|")
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def detect_delimiter(path):
|
|
51
|
-
suffix = path.suffix.lower()
|
|
52
|
-
if suffix == ".tsv":
|
|
53
|
-
return "\t"
|
|
54
|
-
return ","
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def read_rows(path):
|
|
58
|
-
if path is None:
|
|
59
|
-
return []
|
|
60
|
-
path = Path(path)
|
|
61
|
-
if not path.exists():
|
|
62
|
-
raise SystemExit(f"Input file not found: {path}")
|
|
63
|
-
suffix = path.suffix.lower()
|
|
64
|
-
if suffix == ".jsonl":
|
|
65
|
-
rows = []
|
|
66
|
-
with path.open("r", encoding="utf-8-sig", newline="") as handle:
|
|
67
|
-
for line_no, line in enumerate(handle, 1):
|
|
68
|
-
line = line.strip()
|
|
69
|
-
if not line:
|
|
70
|
-
continue
|
|
71
|
-
try:
|
|
72
|
-
value = json.loads(line)
|
|
73
|
-
except json.JSONDecodeError as exc:
|
|
74
|
-
raise SystemExit(f"{path}:{line_no}: invalid JSONL: {exc}") from exc
|
|
75
|
-
if not isinstance(value, dict):
|
|
76
|
-
raise SystemExit(f"{path}:{line_no}: JSONL row must be an object")
|
|
77
|
-
rows.append(value)
|
|
78
|
-
return rows
|
|
79
|
-
if suffix == ".json":
|
|
80
|
-
with path.open("r", encoding="utf-8-sig") as handle:
|
|
81
|
-
value = json.load(handle)
|
|
82
|
-
if isinstance(value, dict):
|
|
83
|
-
for key in ("rows", "targets", "items"):
|
|
84
|
-
if key in value:
|
|
85
|
-
value = value[key]
|
|
86
|
-
break
|
|
87
|
-
if not isinstance(value, list) or any(not isinstance(row, dict) for row in value):
|
|
88
|
-
raise SystemExit(f"{path}: JSON input must be an array of objects or an object with rows")
|
|
89
|
-
return value
|
|
90
|
-
delimiter = detect_delimiter(path)
|
|
91
|
-
with path.open("r", encoding="utf-8-sig", newline="") as handle:
|
|
92
|
-
return list(csv.DictReader(handle, delimiter=delimiter))
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def write_csv(path, rows, columns):
|
|
96
|
-
path = Path(path)
|
|
97
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
98
|
-
with path.open("w", encoding="utf-8", newline="") as handle:
|
|
99
|
-
writer = csv.DictWriter(handle, fieldnames=columns)
|
|
100
|
-
writer.writeheader()
|
|
101
|
-
for row in rows:
|
|
102
|
-
writer.writerow({column: row.get(column, "") for column in columns})
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def parse_prefix_args(values):
|
|
106
|
-
prefixes = dict(DEFAULT_PREFIXES)
|
|
107
|
-
for value in values or []:
|
|
108
|
-
if "=" not in value:
|
|
109
|
-
raise SystemExit(f"Invalid --prefix value: {value}. Expected source_type=PREFIX.")
|
|
110
|
-
source_type, prefix = value.split("=", 1)
|
|
111
|
-
source_type = normalize_text(source_type)
|
|
112
|
-
prefix = normalize_text(prefix).upper()
|
|
113
|
-
if not source_type or not re.fullmatch(r"[A-Z][A-Z0-9]*", prefix):
|
|
114
|
-
raise SystemExit(f"Invalid --prefix value: {value}.")
|
|
115
|
-
prefixes[source_type] = prefix
|
|
116
|
-
return prefixes
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def row_get(row, *names):
|
|
120
|
-
lowered = {str(key).lower(): key for key in row.keys()}
|
|
121
|
-
for name in names:
|
|
122
|
-
key = lowered.get(name.lower())
|
|
123
|
-
if key is not None:
|
|
124
|
-
return row.get(key)
|
|
125
|
-
return ""
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def normalize_targets(rows, prefixes):
|
|
129
|
-
normalized = []
|
|
130
|
-
for index, row in enumerate(rows, 1):
|
|
131
|
-
source_type = normalize_text(row_get(row, "source_type", "type"))
|
|
132
|
-
group = normalize_text(row_get(row, "group", "group_name", "module", "category")) or "Unclassified"
|
|
133
|
-
name = normalize_text(row_get(row, "name", "display_name", "label", "title"))
|
|
134
|
-
entrypoint = normalize_text(row_get(row, "entrypoint", "activation_target", "route", "path", "command"))
|
|
135
|
-
keyword = normalize_text(row_get(row, "keyword", "stable_key", "key", "code", "id"))
|
|
136
|
-
document_path = normalize_text(row_get(row, "document_path", "doc_path"))
|
|
137
|
-
baseline_status = normalize_status(
|
|
138
|
-
row_get(row, "baseline_status"),
|
|
139
|
-
VALID_BASELINE_STATUSES,
|
|
140
|
-
"pending",
|
|
141
|
-
)
|
|
142
|
-
foundation_doc_status = normalize_status(
|
|
143
|
-
row_get(row, "foundation_doc_status"),
|
|
144
|
-
VALID_DOC_STATUSES,
|
|
145
|
-
"pending",
|
|
146
|
-
)
|
|
147
|
-
architecture_doc_status = normalize_status(
|
|
148
|
-
row_get(row, "architecture_doc_status"),
|
|
149
|
-
VALID_DOC_STATUSES,
|
|
150
|
-
"pending",
|
|
151
|
-
)
|
|
152
|
-
ops_doc_status = normalize_status(
|
|
153
|
-
row_get(row, "ops_doc_status"),
|
|
154
|
-
VALID_DOC_STATUSES,
|
|
155
|
-
"pending",
|
|
156
|
-
)
|
|
157
|
-
review_status = normalize_status(
|
|
158
|
-
row_get(row, "review_status"), VALID_REVIEW_STATUSES, "not_started"
|
|
159
|
-
)
|
|
160
|
-
completed_flag = normalize_completed_flag(
|
|
161
|
-
row_get(row, "document_completed_flag(Y/N)")
|
|
162
|
-
)
|
|
163
|
-
if not source_type:
|
|
164
|
-
raise SystemExit(f"Target row {index}: missing source_type")
|
|
165
|
-
if source_type not in prefixes:
|
|
166
|
-
raise SystemExit(
|
|
167
|
-
f"Target row {index}: source_type '{source_type}' has no prefix. "
|
|
168
|
-
"Register the activation surface in Source Type Registry or pass "
|
|
169
|
-
"--prefix source_type=PREFIX for this converter run."
|
|
170
|
-
)
|
|
171
|
-
if not name:
|
|
172
|
-
raise SystemExit(f"Target row {index}: missing name")
|
|
173
|
-
if not keyword:
|
|
174
|
-
keyword = entrypoint or name
|
|
175
|
-
if not entrypoint:
|
|
176
|
-
entrypoint = keyword
|
|
177
|
-
if not document_path:
|
|
178
|
-
pattern = DEFAULT_PATH_PATTERNS.get(source_type, "docs/{source_type}/{name}")
|
|
179
|
-
document_path = pattern.format(
|
|
180
|
-
source_type=normalize_path_segment(source_type),
|
|
181
|
-
group=normalize_path_segment(group),
|
|
182
|
-
name=normalize_path_segment(name),
|
|
183
|
-
)
|
|
184
|
-
doc_profile = normalize_doc_profile(row_get(row, "doc_profile"), source_type)
|
|
185
|
-
normalized.append(
|
|
186
|
-
{
|
|
187
|
-
"id": normalize_text(row_get(row, "id")),
|
|
188
|
-
"source_type": source_type,
|
|
189
|
-
"original_id": normalize_text(row_get(row, "original_id")),
|
|
190
|
-
"group": group,
|
|
191
|
-
"name": name,
|
|
192
|
-
"entrypoint": entrypoint,
|
|
193
|
-
"document_path": document_path,
|
|
194
|
-
"keyword": keyword,
|
|
195
|
-
"doc_profile": doc_profile,
|
|
196
|
-
"baseline_status": baseline_status,
|
|
197
|
-
"foundation_doc_status": foundation_doc_status,
|
|
198
|
-
"architecture_doc_status": architecture_doc_status,
|
|
199
|
-
"ops_doc_status": ops_doc_status,
|
|
200
|
-
"review_status": review_status,
|
|
201
|
-
"document_completed_flag(Y/N)": completed_flag,
|
|
202
|
-
"last_handoff": normalize_text(row_get(row, "last_handoff")),
|
|
203
|
-
"notes": normalize_text(row_get(row, "notes")),
|
|
204
|
-
}
|
|
205
|
-
)
|
|
206
|
-
return dedupe_targets(normalized)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def dedupe_targets(rows):
|
|
210
|
-
deduped = OrderedDict()
|
|
211
|
-
for row in rows:
|
|
212
|
-
key = (
|
|
213
|
-
row["source_type"].casefold(),
|
|
214
|
-
row["keyword"].casefold(),
|
|
215
|
-
row["entrypoint"].casefold(),
|
|
216
|
-
)
|
|
217
|
-
if key not in deduped:
|
|
218
|
-
deduped[key] = row
|
|
219
|
-
continue
|
|
220
|
-
current = deduped[key]
|
|
221
|
-
for column in MAIN_COLUMNS:
|
|
222
|
-
if not current.get(column) and row.get(column):
|
|
223
|
-
current[column] = row[column]
|
|
224
|
-
return list(deduped.values())
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def target_sort_key(row):
|
|
228
|
-
return (
|
|
229
|
-
normalize_text(row.get("source_type", "")).casefold(),
|
|
230
|
-
normalize_text(row.get("group", "")).casefold(),
|
|
231
|
-
normalize_text(row.get("name", "")).casefold(),
|
|
232
|
-
normalize_text(row.get("entrypoint", "")).casefold(),
|
|
233
|
-
normalize_text(row.get("keyword", "")).casefold(),
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
def sort_targets_for_id_assignment(rows):
|
|
238
|
-
return sorted(rows, key=target_sort_key)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
def parse_existing_ids(path):
|
|
242
|
-
if path is None or not Path(path).exists():
|
|
243
|
-
return {}, defaultdict(set)
|
|
244
|
-
rows = parse_markdown_main_table(Path(path))
|
|
245
|
-
mapping = {}
|
|
246
|
-
used = defaultdict(set)
|
|
247
|
-
for row in rows:
|
|
248
|
-
source_type = row.get("source_type", "")
|
|
249
|
-
original_id = row.get("original_id", "")
|
|
250
|
-
if source_type and original_id.isdigit():
|
|
251
|
-
used[source_type].add(int(original_id))
|
|
252
|
-
for key in target_keys(row):
|
|
253
|
-
mapping[key] = {
|
|
254
|
-
"id": row.get("id", ""),
|
|
255
|
-
"original_id": row.get("original_id", ""),
|
|
256
|
-
}
|
|
257
|
-
return mapping, used
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
def parse_markdown_main_table(path):
|
|
261
|
-
rows = []
|
|
262
|
-
lines = path.read_text(encoding="utf-8-sig").splitlines()
|
|
263
|
-
for index, line in enumerate(lines):
|
|
264
|
-
cells = split_markdown_row(line)
|
|
265
|
-
if cells == MAIN_COLUMNS:
|
|
266
|
-
columns = cells
|
|
267
|
-
if index + 1 >= len(lines):
|
|
268
|
-
return rows
|
|
269
|
-
for data_line in lines[index + 2 :]:
|
|
270
|
-
if not data_line.strip().startswith("|"):
|
|
271
|
-
break
|
|
272
|
-
values = split_markdown_row(data_line)
|
|
273
|
-
if len(values) != len(columns):
|
|
274
|
-
break
|
|
275
|
-
row = dict(zip(columns, values))
|
|
276
|
-
rows.append(normalize_existing_row(row))
|
|
277
|
-
break
|
|
278
|
-
return rows
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
def normalize_existing_row(row):
|
|
282
|
-
normalized = {column: row.get(column, "") for column in MAIN_COLUMNS}
|
|
283
|
-
normalized["doc_profile"] = normalize_doc_profile(
|
|
284
|
-
normalized.get("doc_profile", ""), normalized.get("source_type", "")
|
|
285
|
-
)
|
|
286
|
-
return normalized
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
def split_markdown_row(line):
|
|
290
|
-
line = line.strip()
|
|
291
|
-
if not line.startswith("|") or not line.endswith("|"):
|
|
292
|
-
return []
|
|
293
|
-
cells = []
|
|
294
|
-
current = []
|
|
295
|
-
escaped = False
|
|
296
|
-
for char in line[1:-1]:
|
|
297
|
-
if escaped:
|
|
298
|
-
current.append(char)
|
|
299
|
-
escaped = False
|
|
300
|
-
elif char == "\\":
|
|
301
|
-
escaped = True
|
|
302
|
-
elif char == "|":
|
|
303
|
-
cells.append("".join(current).strip())
|
|
304
|
-
current = []
|
|
305
|
-
else:
|
|
306
|
-
current.append(char)
|
|
307
|
-
cells.append("".join(current).strip())
|
|
308
|
-
return cells
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
def target_keys(row):
|
|
312
|
-
source_type = normalize_text(row.get("source_type", "")).casefold()
|
|
313
|
-
keyword = normalize_text(row.get("keyword", "")).casefold()
|
|
314
|
-
entrypoint = normalize_text(row.get("entrypoint", "")).casefold()
|
|
315
|
-
name = normalize_text(row.get("name", "")).casefold()
|
|
316
|
-
keys = []
|
|
317
|
-
if source_type and keyword:
|
|
318
|
-
keys.append((source_type, "keyword", keyword))
|
|
319
|
-
if source_type and entrypoint and name:
|
|
320
|
-
keys.append((source_type, "entrypoint-name", entrypoint, name))
|
|
321
|
-
return keys
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
def assign_ids(rows, prefixes, existing_mapping, used_original_ids):
|
|
325
|
-
used_ids = set()
|
|
326
|
-
next_ids = {}
|
|
327
|
-
for source_type, values in used_original_ids.items():
|
|
328
|
-
next_ids[source_type] = max(values) + 1 if values else 1
|
|
329
|
-
|
|
330
|
-
for row in rows:
|
|
331
|
-
preserved = None
|
|
332
|
-
for key in target_keys(row):
|
|
333
|
-
preserved = existing_mapping.get(key)
|
|
334
|
-
if preserved:
|
|
335
|
-
break
|
|
336
|
-
source_type = row["source_type"]
|
|
337
|
-
prefix = prefixes[source_type]
|
|
338
|
-
if preserved and preserved.get("id") and preserved["id"] not in used_ids:
|
|
339
|
-
row["id"] = preserved["id"]
|
|
340
|
-
row["original_id"] = preserved.get("original_id", row.get("original_id", ""))
|
|
341
|
-
elif row.get("id") and row["id"] not in used_ids:
|
|
342
|
-
row["original_id"] = row.get("original_id") or strip_prefix(row["id"], prefix)
|
|
343
|
-
else:
|
|
344
|
-
next_value = next_ids.get(source_type, 1)
|
|
345
|
-
while next_value in used_original_ids[source_type]:
|
|
346
|
-
next_value += 1
|
|
347
|
-
row["original_id"] = str(next_value)
|
|
348
|
-
row["id"] = f"{prefix}{next_value}"
|
|
349
|
-
used_original_ids[source_type].add(next_value)
|
|
350
|
-
next_ids[source_type] = next_value + 1
|
|
351
|
-
if not row["original_id"].isdigit():
|
|
352
|
-
raise SystemExit(f"Row '{row['name']}' has non-numeric original_id: {row['original_id']}")
|
|
353
|
-
expected_prefix = prefixes[source_type]
|
|
354
|
-
expected_id = f"{expected_prefix}{row['original_id']}"
|
|
355
|
-
if row["id"] != expected_id:
|
|
356
|
-
raise SystemExit(
|
|
357
|
-
f"Row '{row['name']}' id does not match source_type prefix and original_id: "
|
|
358
|
-
f"{row['id']} != {expected_id}"
|
|
359
|
-
)
|
|
360
|
-
if row["id"] in used_ids:
|
|
361
|
-
raise SystemExit(f"Duplicate id after assignment: {row['id']}")
|
|
362
|
-
used_ids.add(row["id"])
|
|
363
|
-
return rows
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
def strip_prefix(value, prefix):
|
|
367
|
-
value = normalize_text(value)
|
|
368
|
-
if value.startswith(prefix):
|
|
369
|
-
return value[len(prefix) :]
|
|
370
|
-
return ""
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
def normalize_summary(rows):
|
|
374
|
-
normalized = []
|
|
375
|
-
for index, row in enumerate(rows, 1):
|
|
376
|
-
item = {column: normalize_text(row_get(row, column)) for column in SUMMARY_COLUMNS}
|
|
377
|
-
for column in ("raw_count", "eligible_count", "excluded_count", "gap_count"):
|
|
378
|
-
if item[column] == "":
|
|
379
|
-
raise SystemExit(f"Summary row {index}: missing {column}")
|
|
380
|
-
if not item[column].isdigit():
|
|
381
|
-
raise SystemExit(f"Summary row {index}: {column} must be a non-negative integer")
|
|
382
|
-
raw = int(item["raw_count"])
|
|
383
|
-
eligible = int(item["eligible_count"])
|
|
384
|
-
excluded = int(item["excluded_count"])
|
|
385
|
-
gap = int(item["gap_count"])
|
|
386
|
-
if raw != eligible + excluded + gap:
|
|
387
|
-
raise SystemExit(
|
|
388
|
-
f"Summary row {index}: raw_count must equal eligible_count + excluded_count + gap_count"
|
|
389
|
-
)
|
|
390
|
-
normalized.append(item)
|
|
391
|
-
return normalized
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
def normalize_coverage(rows):
|
|
395
|
-
normalized = []
|
|
396
|
-
for row in rows:
|
|
397
|
-
item = {column: normalize_text(row_get(row, column)) for column in COVERAGE_COLUMNS}
|
|
398
|
-
if any(item.values()):
|
|
399
|
-
normalized.append(item)
|
|
400
|
-
return normalized
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
def validate_main_counts(rows, summary_rows):
|
|
404
|
-
main_count = len(rows)
|
|
405
|
-
eligible_count = sum(int(row["eligible_count"]) for row in summary_rows)
|
|
406
|
-
if eligible_count != main_count:
|
|
407
|
-
raise SystemExit(
|
|
408
|
-
f"Eligible count mismatch: summary eligible_count total is {eligible_count}, "
|
|
409
|
-
f"but main target rows are {main_count}."
|
|
410
|
-
)
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
def is_canonical_queue_output(output_path):
|
|
414
|
-
return (
|
|
415
|
-
output_path.name == "docs-target-queue.md"
|
|
416
|
-
and output_path.parent.name.casefold() == "docs"
|
|
417
|
-
)
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
def is_target_staging_output(output_path):
|
|
421
|
-
return (
|
|
422
|
-
output_path.suffix.lower() == ".md"
|
|
423
|
-
and "target" in {part.casefold() for part in output_path.parts}
|
|
424
|
-
)
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
def validate_output_path(output_path, partial_output):
|
|
428
|
-
if is_canonical_queue_output(output_path):
|
|
429
|
-
return
|
|
430
|
-
if partial_output and is_target_staging_output(output_path):
|
|
431
|
-
return
|
|
432
|
-
if partial_output:
|
|
433
|
-
raise SystemExit(
|
|
434
|
-
"--partial-output only permits staging Markdown output under target/**. "
|
|
435
|
-
"Final queue output must be docs/docs-target-queue.md."
|
|
436
|
-
)
|
|
437
|
-
raise SystemExit(
|
|
438
|
-
"Final queue output must be docs/docs-target-queue.md. "
|
|
439
|
-
"For partial candidates, pass --partial-output and write under "
|
|
440
|
-
"target/docs-target-queue-from-catalog/."
|
|
441
|
-
)
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
def build_obsidian_links(output_path):
|
|
445
|
-
links = []
|
|
446
|
-
if output_path.name == "docs-target-queue.md":
|
|
447
|
-
catalog_path = output_path.with_name("docs-target-catalog.md")
|
|
448
|
-
if catalog_path.exists():
|
|
449
|
-
links.append("- [[docs-target-catalog]]")
|
|
450
|
-
return links
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
def build_markdown(rows, summary_rows, coverage_rows, source_note, output_path):
|
|
454
|
-
lines = []
|
|
455
|
-
lines.extend(["# Documentation Target Queue", ""])
|
|
456
|
-
lines.append(f"Source note: {source_note}")
|
|
457
|
-
obsidian_links = build_obsidian_links(output_path)
|
|
458
|
-
if obsidian_links:
|
|
459
|
-
lines.extend(["", "## Obsidian Links", ""])
|
|
460
|
-
lines.extend(obsidian_links)
|
|
461
|
-
lines.extend(["", "## Source Acquisition Summary", ""])
|
|
462
|
-
append_table(lines, SUMMARY_COLUMNS, summary_rows)
|
|
463
|
-
lines.extend(["", "## Main Target Table", ""])
|
|
464
|
-
append_table(lines, MAIN_COLUMNS, rows)
|
|
465
|
-
lines.extend(["", "## Source-Type Counts", ""])
|
|
466
|
-
count_rows = []
|
|
467
|
-
counts = defaultdict(int)
|
|
468
|
-
for row in rows:
|
|
469
|
-
counts[row["source_type"]] += 1
|
|
470
|
-
for source_type in sorted(counts):
|
|
471
|
-
count_rows.append({"source_type": source_type, "count": str(counts[source_type])})
|
|
472
|
-
count_rows.append({"source_type": "Total", "count": str(len(rows))})
|
|
473
|
-
append_table(lines, ["source_type", "count"], count_rows)
|
|
474
|
-
lines.extend(["", "## Coverage Review", ""])
|
|
475
|
-
append_table(lines, COVERAGE_COLUMNS, coverage_rows)
|
|
476
|
-
lines.append("")
|
|
477
|
-
return "\n".join(lines)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
def append_table(lines, columns, rows):
|
|
481
|
-
lines.append("| " + " | ".join(columns) + " |")
|
|
482
|
-
lines.append("| " + " | ".join("---" for _ in columns) + " |")
|
|
483
|
-
for row in rows:
|
|
484
|
-
values = [markdown_escape(row.get(column, "")) for column in columns]
|
|
485
|
-
lines.append("| " + " | ".join(values) + " |")
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
def main():
|
|
489
|
-
parser = argparse.ArgumentParser(
|
|
490
|
-
description="Generate docs/docs-target-queue.md from normalized target, summary, and coverage inputs."
|
|
491
|
-
)
|
|
492
|
-
parser.add_argument("--targets", required=True, help="CSV/TSV/JSON/JSONL normalized target rows.")
|
|
493
|
-
parser.add_argument("--summary", required=True, help="CSV/TSV/JSON/JSONL source acquisition summary rows.")
|
|
494
|
-
parser.add_argument("--coverage", help="CSV/TSV/JSON/JSONL coverage review rows.")
|
|
495
|
-
parser.add_argument("--existing", help="Existing docs/docs-target-queue.md used to preserve IDs.")
|
|
496
|
-
parser.add_argument("--output", required=True, help="Output docs/docs-target-queue.md path.")
|
|
497
|
-
parser.add_argument("--source-note", required=True, help="Short source note for the generated Markdown.")
|
|
498
|
-
parser.add_argument(
|
|
499
|
-
"--partial-output",
|
|
500
|
-
action="store_true",
|
|
501
|
-
help=(
|
|
502
|
-
"Allow writing a partial/staging Markdown output under target/**. "
|
|
503
|
-
"Final output remains docs/docs-target-queue.md."
|
|
504
|
-
),
|
|
505
|
-
)
|
|
506
|
-
parser.add_argument(
|
|
507
|
-
"--prefix",
|
|
508
|
-
action="append",
|
|
509
|
-
default=[],
|
|
510
|
-
help=(
|
|
511
|
-
"Registration extension for a source_type prefix in source_type=PREFIX form. "
|
|
512
|
-
"Use for activation surfaces not yet in the shared registry. May be repeated."
|
|
513
|
-
),
|
|
514
|
-
)
|
|
515
|
-
parser.add_argument(
|
|
516
|
-
"--write-normalized-targets",
|
|
517
|
-
help="Optional CSV path for the post-validation target rows with assigned IDs.",
|
|
518
|
-
)
|
|
519
|
-
args = parser.parse_args()
|
|
520
|
-
|
|
521
|
-
output = Path(args.output)
|
|
522
|
-
validate_output_path(output, args.partial_output)
|
|
523
|
-
|
|
524
|
-
prefixes = parse_prefix_args(args.prefix)
|
|
525
|
-
targets = normalize_targets(read_rows(args.targets), prefixes)
|
|
526
|
-
summary = normalize_summary(read_rows(args.summary))
|
|
527
|
-
coverage = normalize_coverage(read_rows(args.coverage) if args.coverage else [])
|
|
528
|
-
existing_mapping, used_original_ids = parse_existing_ids(args.existing)
|
|
529
|
-
targets = sort_targets_for_id_assignment(targets)
|
|
530
|
-
targets = assign_ids(targets, prefixes, existing_mapping, used_original_ids)
|
|
531
|
-
validate_main_counts(targets, summary)
|
|
532
|
-
|
|
533
|
-
output.parent.mkdir(parents=True, exist_ok=True)
|
|
534
|
-
output.write_text(build_markdown(targets, summary, coverage, args.source_note, output), encoding="utf-8", newline="\n")
|
|
535
|
-
if args.write_normalized_targets:
|
|
536
|
-
write_csv(args.write_normalized_targets, targets, MAIN_COLUMNS)
|
|
537
|
-
print(f"Wrote {output} with {len(targets)} target rows.")
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
if __name__ == "__main__":
|
|
541
|
-
try:
|
|
542
|
-
main()
|
|
543
|
-
except BrokenPipeError:
|
|
544
|
-
sys.exit(1)
|