ops-wiki-agent-kit 0.1.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.
Files changed (49) hide show
  1. package/.github/agents/docs-target-catalog.agent.md +52 -0
  2. package/.github/agents/docs-target-queue-from-catalog.agent.md +34 -0
  3. package/.github/agents/source-code-to-spec-documenter.agent.md +39 -0
  4. package/.github/agents/source-code-to-spec-reviewer.agent.md +51 -0
  5. package/.github/agents/source-code-to-system-ops-overview.agent.md +39 -0
  6. package/.github/prompts/00-generate-target-all-spec.prompt.md +35 -0
  7. package/.github/prompts/01-generate-target-foundation-spec.prompt.md +35 -0
  8. package/.github/prompts/02-generate-target-architecture-spec.prompt.md +35 -0
  9. package/.github/prompts/03-generate-target-ops-spec.prompt.md +35 -0
  10. package/.github/prompts/04-review-target-spec.prompt.md +24 -0
  11. package/.github/prompts/docs-target-catalog.prompt.md +32 -0
  12. package/.github/prompts/docs-target-queue-from-catalog.prompt.md +28 -0
  13. package/.github/prompts/generate-system-ops-overview.prompt.md +62 -0
  14. package/.github/skills/database-query/SKILL.md +140 -0
  15. package/.github/skills/database-query/references/client-commands.md +189 -0
  16. package/.github/skills/database-query/references/query-safety.md +109 -0
  17. package/.github/skills/database-query/scripts/find_db_config.py +273 -0
  18. package/.github/skills/docs-target-catalog/SKILL.md +194 -0
  19. package/.github/skills/docs-target-catalog/references/docs-target-queue-conversion.md +164 -0
  20. package/.github/skills/docs-target-catalog/references/entrypoint-source-patterns.md +83 -0
  21. package/.github/skills/docs-target-catalog/references/output-templates.md +168 -0
  22. package/.github/skills/docs-target-queue-from-catalog/SKILL.md +255 -0
  23. package/.github/skills/docs-target-queue-from-catalog/references/docs-target-queue-contract.md +125 -0
  24. package/.github/skills/docs-target-queue-from-catalog/references/metadata-acquisition-patterns.md +149 -0
  25. package/.github/skills/docs-target-queue-from-catalog/scripts/write_documentation_target_queue.py +527 -0
  26. package/.github/skills/source-code-to-ops-spec-guidelines/SKILL.md +128 -0
  27. package/.github/skills/source-code-to-ops-spec-guidelines/references/01-system-overview-and-business-scenarios-guideline.md +172 -0
  28. package/.github/skills/source-code-to-ops-spec-guidelines/references/02-core-architecture-flow-data-logic-guideline.md +637 -0
  29. package/.github/skills/source-code-to-ops-spec-guidelines/references/03-error-ops-scenario-coverage-guideline.md +533 -0
  30. package/.github/skills/source-code-to-ops-spec-guidelines/references/supporting-output-format-diagram-and-example-reference.md +523 -0
  31. package/.github/skills/source-code-to-spec-documenter/SKILL.md +80 -0
  32. package/.github/skills/source-code-to-spec-documenter/references/generation-handoff-contract.md +155 -0
  33. package/.github/skills/source-code-to-spec-documenter/references/generation-workflow.md +184 -0
  34. package/.github/skills/source-code-to-spec-documenter/references/source-tracing-rules.md +271 -0
  35. package/.github/skills/source-code-to-spec-documenter/references/target-queue-contract.md +78 -0
  36. package/.github/skills/source-code-to-spec-documenter/scripts/spec_queue.py +222 -0
  37. package/.github/skills/source-code-to-spec-tools/SKILL.md +117 -0
  38. package/.github/skills/source-code-to-spec-tools/references/repository-artifact-contract.md +122 -0
  39. package/.github/skills/source-code-to-spec-tools/references/target-queue-schema-contract.md +116 -0
  40. package/.github/skills/source-code-to-spec-tools/references/terminology-contract.md +121 -0
  41. package/.github/skills/source-code-to-spec-tools/scripts/catalog_query.py +324 -0
  42. package/.github/skills/source-code-to-spec-tools/scripts/queue_contract.py +210 -0
  43. package/.github/skills/source-code-to-spec-tools/scripts/source_lookup.py +360 -0
  44. package/.github/skills/source-code-to-spec-tools/scripts/target_query.py +407 -0
  45. package/.github/skills/source-code-to-system-ops-overview/SKILL.md +82 -0
  46. package/.github/skills/source-code-to-system-ops-overview/references/system-operations-overview-guideline.md +332 -0
  47. package/README.md +116 -0
  48. package/ops-wiki-agent-kit.js +173 -0
  49. package/package.json +22 -0
@@ -0,0 +1,527 @@
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
+ DOC_STATUSES as VALID_DOC_STATUSES,
23
+ COVERAGE_COLUMNS,
24
+ DEFAULT_PATH_PATTERNS,
25
+ DEFAULT_PREFIXES,
26
+ MAIN_COLUMNS,
27
+ REVIEW_STATUSES as VALID_REVIEW_STATUSES,
28
+ SUMMARY_COLUMNS,
29
+ normalize_completed_flag,
30
+ normalize_doc_profile,
31
+ normalize_status,
32
+ normalize_text,
33
+ )
34
+
35
+
36
+ def normalize_path_segment(value):
37
+ value = normalize_text(value)
38
+ if not value:
39
+ return "Unclassified"
40
+ value = re.sub(r'[<>:"/\\|?*]', " ", value)
41
+ value = re.sub(r"\s+", " ", value).strip()
42
+ return value or "Unclassified"
43
+
44
+
45
+ def markdown_escape(value):
46
+ return normalize_text(value).replace("|", "\\|")
47
+
48
+
49
+ def detect_delimiter(path):
50
+ suffix = path.suffix.lower()
51
+ if suffix == ".tsv":
52
+ return "\t"
53
+ return ","
54
+
55
+
56
+ def read_rows(path):
57
+ if path is None:
58
+ return []
59
+ path = Path(path)
60
+ if not path.exists():
61
+ raise SystemExit(f"Input file not found: {path}")
62
+ suffix = path.suffix.lower()
63
+ if suffix == ".jsonl":
64
+ rows = []
65
+ with path.open("r", encoding="utf-8-sig", newline="") as handle:
66
+ for line_no, line in enumerate(handle, 1):
67
+ line = line.strip()
68
+ if not line:
69
+ continue
70
+ try:
71
+ value = json.loads(line)
72
+ except json.JSONDecodeError as exc:
73
+ raise SystemExit(f"{path}:{line_no}: invalid JSONL: {exc}") from exc
74
+ if not isinstance(value, dict):
75
+ raise SystemExit(f"{path}:{line_no}: JSONL row must be an object")
76
+ rows.append(value)
77
+ return rows
78
+ if suffix == ".json":
79
+ with path.open("r", encoding="utf-8-sig") as handle:
80
+ value = json.load(handle)
81
+ if isinstance(value, dict):
82
+ for key in ("rows", "targets", "items"):
83
+ if key in value:
84
+ value = value[key]
85
+ break
86
+ if not isinstance(value, list) or any(not isinstance(row, dict) for row in value):
87
+ raise SystemExit(f"{path}: JSON input must be an array of objects or an object with rows")
88
+ return value
89
+ delimiter = detect_delimiter(path)
90
+ with path.open("r", encoding="utf-8-sig", newline="") as handle:
91
+ return list(csv.DictReader(handle, delimiter=delimiter))
92
+
93
+
94
+ def write_csv(path, rows, columns):
95
+ path = Path(path)
96
+ path.parent.mkdir(parents=True, exist_ok=True)
97
+ with path.open("w", encoding="utf-8", newline="") as handle:
98
+ writer = csv.DictWriter(handle, fieldnames=columns)
99
+ writer.writeheader()
100
+ for row in rows:
101
+ writer.writerow({column: row.get(column, "") for column in columns})
102
+
103
+
104
+ def parse_prefix_args(values):
105
+ prefixes = dict(DEFAULT_PREFIXES)
106
+ for value in values or []:
107
+ if "=" not in value:
108
+ raise SystemExit(f"Invalid --prefix value: {value}. Expected source_type=PREFIX.")
109
+ source_type, prefix = value.split("=", 1)
110
+ source_type = normalize_text(source_type)
111
+ prefix = normalize_text(prefix).upper()
112
+ if not source_type or not re.fullmatch(r"[A-Z][A-Z0-9]*", prefix):
113
+ raise SystemExit(f"Invalid --prefix value: {value}.")
114
+ prefixes[source_type] = prefix
115
+ return prefixes
116
+
117
+
118
+ def row_get(row, *names):
119
+ lowered = {str(key).lower(): key for key in row.keys()}
120
+ for name in names:
121
+ key = lowered.get(name.lower())
122
+ if key is not None:
123
+ return row.get(key)
124
+ return ""
125
+
126
+
127
+ def normalize_targets(rows, prefixes):
128
+ normalized = []
129
+ for index, row in enumerate(rows, 1):
130
+ source_type = normalize_text(row_get(row, "source_type", "type"))
131
+ group = normalize_text(row_get(row, "group", "group_name", "module", "category")) or "Unclassified"
132
+ name = normalize_text(row_get(row, "name", "display_name", "label", "title"))
133
+ entrypoint = normalize_text(row_get(row, "entrypoint", "activation_target", "route", "path", "command"))
134
+ keyword = normalize_text(row_get(row, "keyword", "stable_key", "key", "code", "id"))
135
+ document_path = normalize_text(row_get(row, "document_path", "doc_path"))
136
+ foundation_doc_status = normalize_status(
137
+ row_get(row, "foundation_doc_status"),
138
+ VALID_DOC_STATUSES,
139
+ "pending",
140
+ )
141
+ architecture_doc_status = normalize_status(
142
+ row_get(row, "architecture_doc_status"),
143
+ VALID_DOC_STATUSES,
144
+ "pending",
145
+ )
146
+ ops_doc_status = normalize_status(
147
+ row_get(row, "ops_doc_status"),
148
+ VALID_DOC_STATUSES,
149
+ "pending",
150
+ )
151
+ review_status = normalize_status(
152
+ row_get(row, "review_status"), VALID_REVIEW_STATUSES, "not_started"
153
+ )
154
+ completed_flag = normalize_completed_flag(
155
+ row_get(row, "document_completed_flag(Y/N)")
156
+ )
157
+ if not source_type:
158
+ raise SystemExit(f"Target row {index}: missing source_type")
159
+ if source_type not in prefixes:
160
+ raise SystemExit(
161
+ f"Target row {index}: source_type '{source_type}' has no prefix. "
162
+ "Register the activation surface in Source Type Registry or pass "
163
+ "--prefix source_type=PREFIX for this converter run."
164
+ )
165
+ if not name:
166
+ raise SystemExit(f"Target row {index}: missing name")
167
+ if not keyword:
168
+ keyword = entrypoint or name
169
+ if not entrypoint:
170
+ entrypoint = keyword
171
+ if not document_path:
172
+ pattern = DEFAULT_PATH_PATTERNS.get(source_type, "docs/{source_type}/{name}")
173
+ document_path = pattern.format(
174
+ source_type=normalize_path_segment(source_type),
175
+ group=normalize_path_segment(group),
176
+ name=normalize_path_segment(name),
177
+ )
178
+ doc_profile = normalize_doc_profile(row_get(row, "doc_profile"), source_type)
179
+ normalized.append(
180
+ {
181
+ "id": normalize_text(row_get(row, "id")),
182
+ "source_type": source_type,
183
+ "original_id": normalize_text(row_get(row, "original_id")),
184
+ "group": group,
185
+ "name": name,
186
+ "entrypoint": entrypoint,
187
+ "document_path": document_path,
188
+ "keyword": keyword,
189
+ "doc_profile": doc_profile,
190
+ "foundation_doc_status": foundation_doc_status,
191
+ "architecture_doc_status": architecture_doc_status,
192
+ "ops_doc_status": ops_doc_status,
193
+ "review_status": review_status,
194
+ "document_completed_flag(Y/N)": completed_flag,
195
+ "last_handoff": normalize_text(row_get(row, "last_handoff")),
196
+ "notes": normalize_text(row_get(row, "notes")),
197
+ }
198
+ )
199
+ return dedupe_targets(normalized)
200
+
201
+
202
+ def dedupe_targets(rows):
203
+ deduped = OrderedDict()
204
+ for row in rows:
205
+ key = (
206
+ row["source_type"].casefold(),
207
+ row["keyword"].casefold(),
208
+ row["entrypoint"].casefold(),
209
+ )
210
+ if key not in deduped:
211
+ deduped[key] = row
212
+ continue
213
+ current = deduped[key]
214
+ for column in MAIN_COLUMNS:
215
+ if not current.get(column) and row.get(column):
216
+ current[column] = row[column]
217
+ return list(deduped.values())
218
+
219
+
220
+ def sort_targets_by_source_type(rows):
221
+ return sorted(rows, key=lambda row: normalize_text(row.get("source_type", "")).casefold())
222
+
223
+
224
+ def parse_existing_ids(path):
225
+ if path is None or not Path(path).exists():
226
+ return {}, defaultdict(set)
227
+ rows = parse_markdown_main_table(Path(path))
228
+ mapping = {}
229
+ used = defaultdict(set)
230
+ for row in rows:
231
+ source_type = row.get("source_type", "")
232
+ original_id = row.get("original_id", "")
233
+ if source_type and original_id.isdigit():
234
+ used[source_type].add(int(original_id))
235
+ for key in target_keys(row):
236
+ mapping[key] = {
237
+ "id": row.get("id", ""),
238
+ "original_id": row.get("original_id", ""),
239
+ }
240
+ return mapping, used
241
+
242
+
243
+ def parse_markdown_main_table(path):
244
+ rows = []
245
+ lines = path.read_text(encoding="utf-8-sig").splitlines()
246
+ for index, line in enumerate(lines):
247
+ cells = split_markdown_row(line)
248
+ if cells == MAIN_COLUMNS:
249
+ columns = cells
250
+ if index + 1 >= len(lines):
251
+ return rows
252
+ for data_line in lines[index + 2 :]:
253
+ if not data_line.strip().startswith("|"):
254
+ break
255
+ values = split_markdown_row(data_line)
256
+ if len(values) != len(columns):
257
+ break
258
+ row = dict(zip(columns, values))
259
+ rows.append(normalize_existing_row(row))
260
+ break
261
+ return rows
262
+
263
+
264
+ def normalize_existing_row(row):
265
+ normalized = {column: row.get(column, "") for column in MAIN_COLUMNS}
266
+ normalized["doc_profile"] = normalize_doc_profile(
267
+ normalized.get("doc_profile", ""), normalized.get("source_type", "")
268
+ )
269
+ return normalized
270
+
271
+
272
+ def split_markdown_row(line):
273
+ line = line.strip()
274
+ if not line.startswith("|") or not line.endswith("|"):
275
+ return []
276
+ cells = []
277
+ current = []
278
+ escaped = False
279
+ for char in line[1:-1]:
280
+ if escaped:
281
+ current.append(char)
282
+ escaped = False
283
+ elif char == "\\":
284
+ escaped = True
285
+ elif char == "|":
286
+ cells.append("".join(current).strip())
287
+ current = []
288
+ else:
289
+ current.append(char)
290
+ cells.append("".join(current).strip())
291
+ return cells
292
+
293
+
294
+ def target_keys(row):
295
+ source_type = normalize_text(row.get("source_type", "")).casefold()
296
+ keyword = normalize_text(row.get("keyword", "")).casefold()
297
+ entrypoint = normalize_text(row.get("entrypoint", "")).casefold()
298
+ name = normalize_text(row.get("name", "")).casefold()
299
+ keys = []
300
+ if source_type and keyword:
301
+ keys.append((source_type, "keyword", keyword))
302
+ if source_type and entrypoint and name:
303
+ keys.append((source_type, "entrypoint-name", entrypoint, name))
304
+ return keys
305
+
306
+
307
+ def assign_ids(rows, prefixes, existing_mapping, used_original_ids):
308
+ used_ids = set()
309
+ next_ids = {}
310
+ for source_type, values in used_original_ids.items():
311
+ next_ids[source_type] = max(values) + 1 if values else 1
312
+
313
+ for row in rows:
314
+ preserved = None
315
+ for key in target_keys(row):
316
+ preserved = existing_mapping.get(key)
317
+ if preserved:
318
+ break
319
+ source_type = row["source_type"]
320
+ prefix = prefixes[source_type]
321
+ if preserved and preserved.get("id") and preserved["id"] not in used_ids:
322
+ row["id"] = preserved["id"]
323
+ row["original_id"] = preserved.get("original_id", row.get("original_id", ""))
324
+ elif row.get("id") and row["id"] not in used_ids:
325
+ row["original_id"] = row.get("original_id") or strip_prefix(row["id"], prefix)
326
+ else:
327
+ next_value = next_ids.get(source_type, 1)
328
+ while next_value in used_original_ids[source_type]:
329
+ next_value += 1
330
+ row["original_id"] = str(next_value)
331
+ row["id"] = f"{prefix}{next_value}"
332
+ used_original_ids[source_type].add(next_value)
333
+ next_ids[source_type] = next_value + 1
334
+ if not row["original_id"].isdigit():
335
+ raise SystemExit(f"Row '{row['name']}' has non-numeric original_id: {row['original_id']}")
336
+ expected_prefix = prefixes[source_type]
337
+ expected_id = f"{expected_prefix}{row['original_id']}"
338
+ if row["id"] != expected_id:
339
+ raise SystemExit(
340
+ f"Row '{row['name']}' id does not match source_type prefix and original_id: "
341
+ f"{row['id']} != {expected_id}"
342
+ )
343
+ if row["id"] in used_ids:
344
+ raise SystemExit(f"Duplicate id after assignment: {row['id']}")
345
+ used_ids.add(row["id"])
346
+ return rows
347
+
348
+
349
+ def strip_prefix(value, prefix):
350
+ value = normalize_text(value)
351
+ if value.startswith(prefix):
352
+ return value[len(prefix) :]
353
+ return ""
354
+
355
+
356
+ def normalize_summary(rows):
357
+ normalized = []
358
+ for index, row in enumerate(rows, 1):
359
+ item = {column: normalize_text(row_get(row, column)) for column in SUMMARY_COLUMNS}
360
+ for column in ("raw_count", "eligible_count", "excluded_count", "gap_count"):
361
+ if item[column] == "":
362
+ raise SystemExit(f"Summary row {index}: missing {column}")
363
+ if not item[column].isdigit():
364
+ raise SystemExit(f"Summary row {index}: {column} must be a non-negative integer")
365
+ raw = int(item["raw_count"])
366
+ eligible = int(item["eligible_count"])
367
+ excluded = int(item["excluded_count"])
368
+ gap = int(item["gap_count"])
369
+ if raw != eligible + excluded + gap:
370
+ raise SystemExit(
371
+ f"Summary row {index}: raw_count must equal eligible_count + excluded_count + gap_count"
372
+ )
373
+ normalized.append(item)
374
+ return normalized
375
+
376
+
377
+ def normalize_coverage(rows):
378
+ normalized = []
379
+ for row in rows:
380
+ item = {column: normalize_text(row_get(row, column)) for column in COVERAGE_COLUMNS}
381
+ if any(item.values()):
382
+ normalized.append(item)
383
+ return normalized
384
+
385
+
386
+ def validate_main_counts(rows, summary_rows):
387
+ main_count = len(rows)
388
+ eligible_count = sum(int(row["eligible_count"]) for row in summary_rows)
389
+ if eligible_count != main_count:
390
+ raise SystemExit(
391
+ f"Eligible count mismatch: summary eligible_count total is {eligible_count}, "
392
+ f"but main target rows are {main_count}."
393
+ )
394
+
395
+
396
+ def is_canonical_queue_output(output_path):
397
+ return (
398
+ output_path.name == "docs-target-queue.md"
399
+ and output_path.parent.name.casefold() == "docs"
400
+ )
401
+
402
+
403
+ def is_target_staging_output(output_path):
404
+ return (
405
+ output_path.suffix.lower() == ".md"
406
+ and "target" in {part.casefold() for part in output_path.parts}
407
+ )
408
+
409
+
410
+ def validate_output_path(output_path, partial_output):
411
+ if is_canonical_queue_output(output_path):
412
+ return
413
+ if partial_output and is_target_staging_output(output_path):
414
+ return
415
+ if partial_output:
416
+ raise SystemExit(
417
+ "--partial-output only permits staging Markdown output under target/**. "
418
+ "Final queue output must be docs/docs-target-queue.md."
419
+ )
420
+ raise SystemExit(
421
+ "Final queue output must be docs/docs-target-queue.md. "
422
+ "For partial candidates, pass --partial-output and write under "
423
+ "target/docs-target-queue-from-catalog/."
424
+ )
425
+
426
+
427
+ def build_obsidian_links(output_path):
428
+ links = []
429
+ if output_path.name == "docs-target-queue.md":
430
+ catalog_path = output_path.with_name("docs-target-catalog.md")
431
+ if catalog_path.exists():
432
+ links.append("- [[docs-target-catalog]]")
433
+ return links
434
+
435
+
436
+ def build_markdown(rows, summary_rows, coverage_rows, source_note, output_path):
437
+ lines = []
438
+ lines.extend(["# Documentation Target Queue", ""])
439
+ lines.append(f"Source note: {source_note}")
440
+ obsidian_links = build_obsidian_links(output_path)
441
+ if obsidian_links:
442
+ lines.extend(["", "## Obsidian Links", ""])
443
+ lines.extend(obsidian_links)
444
+ lines.extend(["", "## Source Acquisition Summary", ""])
445
+ append_table(lines, SUMMARY_COLUMNS, summary_rows)
446
+ lines.extend(["", "## Main Target Table", ""])
447
+ append_table(lines, MAIN_COLUMNS, rows)
448
+ lines.extend(["", "## Source-Type Counts", ""])
449
+ count_rows = []
450
+ counts = defaultdict(int)
451
+ for row in rows:
452
+ counts[row["source_type"]] += 1
453
+ for source_type in sorted(counts):
454
+ count_rows.append({"source_type": source_type, "count": str(counts[source_type])})
455
+ count_rows.append({"source_type": "Total", "count": str(len(rows))})
456
+ append_table(lines, ["source_type", "count"], count_rows)
457
+ lines.extend(["", "## Coverage Review", ""])
458
+ append_table(lines, COVERAGE_COLUMNS, coverage_rows)
459
+ lines.append("")
460
+ return "\n".join(lines)
461
+
462
+
463
+ def append_table(lines, columns, rows):
464
+ lines.append("| " + " | ".join(columns) + " |")
465
+ lines.append("| " + " | ".join("---" for _ in columns) + " |")
466
+ for row in rows:
467
+ values = [markdown_escape(row.get(column, "")) for column in columns]
468
+ lines.append("| " + " | ".join(values) + " |")
469
+
470
+
471
+ def main():
472
+ parser = argparse.ArgumentParser(
473
+ description="Generate docs/docs-target-queue.md from normalized target, summary, and coverage inputs."
474
+ )
475
+ parser.add_argument("--targets", required=True, help="CSV/TSV/JSON/JSONL normalized target rows.")
476
+ parser.add_argument("--summary", required=True, help="CSV/TSV/JSON/JSONL source acquisition summary rows.")
477
+ parser.add_argument("--coverage", help="CSV/TSV/JSON/JSONL coverage review rows.")
478
+ parser.add_argument("--existing", help="Existing docs/docs-target-queue.md used to preserve IDs.")
479
+ parser.add_argument("--output", required=True, help="Output docs/docs-target-queue.md path.")
480
+ parser.add_argument("--source-note", required=True, help="Short source note for the generated Markdown.")
481
+ parser.add_argument(
482
+ "--partial-output",
483
+ action="store_true",
484
+ help=(
485
+ "Allow writing a partial/staging Markdown output under target/**. "
486
+ "Final output remains docs/docs-target-queue.md."
487
+ ),
488
+ )
489
+ parser.add_argument(
490
+ "--prefix",
491
+ action="append",
492
+ default=[],
493
+ help=(
494
+ "Registration extension for a source_type prefix in source_type=PREFIX form. "
495
+ "Use for activation surfaces not yet in the shared registry. May be repeated."
496
+ ),
497
+ )
498
+ parser.add_argument(
499
+ "--write-normalized-targets",
500
+ help="Optional CSV path for the post-validation target rows with assigned IDs.",
501
+ )
502
+ args = parser.parse_args()
503
+
504
+ output = Path(args.output)
505
+ validate_output_path(output, args.partial_output)
506
+
507
+ prefixes = parse_prefix_args(args.prefix)
508
+ targets = normalize_targets(read_rows(args.targets), prefixes)
509
+ summary = normalize_summary(read_rows(args.summary))
510
+ coverage = normalize_coverage(read_rows(args.coverage) if args.coverage else [])
511
+ existing_mapping, used_original_ids = parse_existing_ids(args.existing)
512
+ targets = assign_ids(targets, prefixes, existing_mapping, used_original_ids)
513
+ targets = sort_targets_by_source_type(targets)
514
+ validate_main_counts(targets, summary)
515
+
516
+ output.parent.mkdir(parents=True, exist_ok=True)
517
+ output.write_text(build_markdown(targets, summary, coverage, args.source_note, output), encoding="utf-8", newline="\n")
518
+ if args.write_normalized_targets:
519
+ write_csv(args.write_normalized_targets, targets, MAIN_COLUMNS)
520
+ print(f"Wrote {output} with {len(targets)} target rows.")
521
+
522
+
523
+ if __name__ == "__main__":
524
+ try:
525
+ main()
526
+ except BrokenPipeError:
527
+ sys.exit(1)
@@ -0,0 +1,128 @@
1
+ ---
2
+ name: source-code-to-ops-spec-guidelines
3
+ description: 在產生、維護、重構或審查 source-code-backed ops SPEC guideline contract 與 reference guideline 時使用。適用於調整 `SKILL.md`、`references/01-system-overview-and-business-scenarios-guideline.md`、`references/02-core-architecture-flow-data-logic-guideline.md`、`references/03-error-ops-scenario-coverage-guideline.md`、圖表可讀性、Metadata、Mermaid syntax、維運導向 SPEC 章節契約、source evidence discipline 與 mixed-language documentation rules。
4
+ ---
5
+
6
+ # Source Code-backed Ops SPEC Guideline Maintainer
7
+
8
+ ## 核心用途
9
+
10
+ 使用這個 skill 來產生、維護、重構或審查 source-code-backed ops SPEC 的 guideline contract 與 reference guideline。它不是用來直接產出某一份最終系統文件;它負責維護「AI 如何根據 source code、SQL、config、DB object、runtime evidence 與既有文件產生維運導向 SPEC」的 guideline contract。
11
+
12
+ 輸出與說明使用繁體中文。Source code、class names、method names、variable names、API paths、SQL、database objects、YAML/JSON/XML keys、CLI commands、Git commands、protocol names、tool names、prompt placeholders、Markdown syntax 與 Mermaid syntax 必須保留英文或原始語法。
13
+
14
+ ## Reference Guidelines
15
+
16
+ 本 skill 的主要 reference guideline 是以下三份。Supporting reference 負責跨三份 target SPEC 共用的文件基礎與輸出格式,不是第四份輸出文件。
17
+
18
+ | Reference | Responsibility |
19
+ |---|---|
20
+ | `references/01-system-overview-and-business-scenarios-guideline.md` | 系統單元概述、使用者問題與業務場景。 |
21
+ | `references/02-core-architecture-flow-data-logic-guideline.md` | 系統責任邊界、進入點、處理流程、資料關聯、資料流向、SQL、狀態、詳細邏輯與資料對應。 |
22
+ | `references/03-error-ops-scenario-coverage-guideline.md` | 錯誤處理、連動影響、log、監控、troubleshooting、維運操作資訊、情境專屬覆蓋與安全證據。 |
23
+ | `references/supporting-output-format-diagram-and-example-reference.md` | Supporting reference;維護跨三份 SPEC 的文件定位、系統單元型態、章節適用判斷、Metadata、Obsidian Links、章節編號原則、圖表可讀性、Mermaid / ASCII Art 語法、圖表文字說明、系統功能規格書 Metadata variant,以及歷史範例整理出的格式規則;本檔不是第四份輸出文件。 |
24
+
25
+ 若未來新增 reference、prompt、agent 或 generated-output contract,必須先確認檔案存在與責任邊界,再把它們加入本 skill。
26
+
27
+ ## Reference Routing
28
+
29
+ 先判斷使用者正在修改哪一種 ops SPEC guideline contract,再只載入相關 reference。
30
+
31
+ | Goal | Load first | Then load if needed |
32
+ |---|---|---|
33
+ | 調整跨三份 SPEC 的文件定位、Metadata、Obsidian Links、系統單元型態或章節適用判斷 | `references/supporting-output-format-diagram-and-example-reference.md` | The owner guideline for the chapter where the rule is applied |
34
+ | 調整系統概述、使用者問題或業務場景 | `references/01-system-overview-and-business-scenarios-guideline.md` | `references/supporting-output-format-diagram-and-example-reference.md` if shared document foundation or Metadata is affected |
35
+ | 調整架構、流程、資料、SQL、狀態或 step-by-step logic | `references/02-core-architecture-flow-data-logic-guideline.md` | `references/supporting-output-format-diagram-and-example-reference.md` if diagram syntax or examples are affected |
36
+ | 調整錯誤、監控、troubleshooting、維運操作或情境覆蓋 | `references/03-error-ops-scenario-coverage-guideline.md` | `references/02-core-architecture-flow-data-logic-guideline.md` if flow-level error behavior must stay aligned |
37
+ | 調整 Mermaid、ASCII Art、圖表文字說明或範例語法 | `references/supporting-output-format-diagram-and-example-reference.md` | The owner guideline for the chapter where the rule is applied |
38
+ | Review guideline drift or stale references | `SKILL.md` plus the suspected owner guideline | Other guidelines only if cross-reference text must be aligned |
39
+
40
+ 不要因為 skill 觸發就全文載入所有 reference。先看任務目標與檔名,再做 progressive loading。
41
+
42
+ ## Generation Usage
43
+
44
+ 若 generation workflow 使用本 skill 產生 target-facing SPEC,必須先套用 `references/supporting-output-format-diagram-and-example-reference.md` 中跨三份 SPEC 的文件基礎規則,再依 `generation_scope` 載入對應 reference guideline;不要把 single-scope invocation 擴大成 all-scope generation。
45
+
46
+ | `generation_scope` | Required reference guideline |
47
+ |---|---|
48
+ | `all` | 依序套用 `references/supporting-output-format-diagram-and-example-reference.md` 的共用文件基礎規則,再載入 `references/01-system-overview-and-business-scenarios-guideline.md`、`references/02-core-architecture-flow-data-logic-guideline.md`、`references/03-error-ops-scenario-coverage-guideline.md`。 |
49
+ | `foundation` | 套用 `references/supporting-output-format-diagram-and-example-reference.md` 的共用文件基礎規則,並載入 `references/01-system-overview-and-business-scenarios-guideline.md`;架構、資料、錯誤與維運內容若影響 foundation 判斷,保留 source-backed cross-reference、handoff 與必要證據線索,但不把其他 scope 的正式章節複製到本文件。 |
50
+ | `architecture` | 套用 `references/supporting-output-format-diagram-and-example-reference.md` 的共用文件基礎規則,並載入 `references/02-core-architecture-flow-data-logic-guideline.md`;foundation 與 ops 內容若影響 architecture 判斷,保留 source-backed cross-reference、handoff 與必要證據線索,但不把其他 scope 的正式章節複製到本文件。 |
51
+ | `ops` | 套用 `references/supporting-output-format-diagram-and-example-reference.md` 的共用文件基礎規則,並載入 `references/03-error-ops-scenario-coverage-guideline.md`;foundation 與 architecture 內容若影響 ops 判斷,保留 source-backed cross-reference、handoff 與必要證據線索,但不把其他 scope 的正式章節複製到本文件。 |
52
+
53
+ 若任務只維護某一份主要 guideline 且不涉及共用文件基礎,可先載入該 owner guideline;涉及 target-facing SPEC generation、Metadata、Obsidian Links、章節適用或圖表格式時,必須載入 `references/supporting-output-format-diagram-and-example-reference.md`。
54
+
55
+ `doc_file_plan` 用來記錄 generation scope、selected output files、章節 `N/A` / `Unresolved` 原因、coverage gaps 與 handoff impact;reference 檔案選擇以本 skill 的 Reference Routing 為準。
56
+
57
+ Target-facing SPEC 的內容優先順序是完整、準確、source-backed,其次才是篇幅壓縮或版面簡潔。摘要、維運摘要卡、圖表文字說明、handoff summary 或 cross-reference 只能協助導覽,不可取代正式詳細章節;若來源可確認 handler、service、DAO / Repository / Mapper、SQL、table、status transition、config、job、file I/O 或 external integration 行為,應在對應 scope 的章節保留可審查細節。
58
+
59
+ Target-facing SPEC 以業務行為、處理流程、資料規則、錯誤處理與維運判斷方式為主。Source path、line、symbol 與 `EvidenceRef[]` 由 `source-code-to-spec-documenter` 的 `handoff.json` 保存;文件本身只在內容需要時保留必要識別資料,例如 API path、SQL ID、table、status column、job name、config key、file name、log keyword 或 trace id,且應整合到對應章節的操作、資料、狀態或錯誤欄位中。
60
+
61
+ `doc_profile=full` 代表 selected scope 內採完整深度,不是把 source-backed detail 壓成摘要。Full profile 至少應保留:
62
+
63
+ - 可從 source 讀取的完整 SQL excerpt;若 SQL 是動態組合,保留 source-backed pseudo SQL、組合條件、參數來源與限制。
64
+ - DAO / Repository / Mapper method 與 SQL、table、status column、DB object、stored routine 或 external resource 的 mapping。
65
+ - Handler / controller / job / service 到 DAO method 的可追蹤流程,以及與維運判斷有關的 branch、transaction、rollback、retry、rerun、log、error message 與 status transition。
66
+ - 欄位 mapping、狀態 mapping、request / response mapping、file column mapping 或 external payload mapping 的來源欄位、目標欄位、轉換規則與限制。
67
+
68
+ 不要因為流程、SQL、圖表或資料 mapping 較大就刪減 source-backed detail;必要時拆成多個表格、子章節或多張圖表,並讓每一段都可追溯到 source evidence。
69
+
70
+ Target-facing SPEC 的每一個正式章節都應保留章節標題。若該章節沒有來源資料、已確認不適用,或目前來源不足以填寫,不要硬湊表格、圖表或推測內容;在章節下方輸出一行 `N/A: <簡短原因>`、`無法由目前來源確認: <缺少的 evidence>` 或 `Unresolved: <阻塞原因>` 即可,並將 completion-impacting gap 寫入 handoff。
71
+
72
+ Target-facing SPEC 的 generation / update 執行流程、source tracing、idempotent update、handoff 欄位與 queue 狀態不由本 skill 維護;請使用 `source-code-to-spec-documenter/references/generation-workflow.md`、`source-code-to-spec-documenter/references/source-tracing-rules.md`、`source-code-to-spec-documenter/references/generation-handoff-contract.md` 與 `source-code-to-spec-documenter/references/target-queue-contract.md`。
73
+
74
+ ## Ownership Boundary
75
+
76
+ 維護 guideline contract 時,先找 owner,再改 owner。避免同一條規則散落在多份文件中。
77
+
78
+ | Rule Type | Owner |
79
+ |---|---|
80
+ | Skill trigger、reference routing、共通維護流程、品質門檻 | `SKILL.md` |
81
+ | Cross-SPEC document positioning、system unit type、section applicability、Metadata、Obsidian Links、chapter numbering | `references/supporting-output-format-diagram-and-example-reference.md` |
82
+ | Overview、user problems、business scenarios | `references/01-system-overview-and-business-scenarios-guideline.md` |
83
+ | Responsibility boundary、entry points、processing flow、data relationship、data flow、SQL、status、steps、mapping | `references/02-core-architecture-flow-data-logic-guideline.md` |
84
+ | Error handling、side effects、logs、monitoring、troubleshooting、operations、scenario coverage、security evidence | `references/03-error-ops-scenario-coverage-guideline.md` |
85
+ | Diagram accessibility、Mermaid syntax caveats、ASCII Art fallback、example tables、system-function-spec Metadata variant | `references/supporting-output-format-diagram-and-example-reference.md` |
86
+ | Existing output reconciliation、idempotent update、handoff coverage status | `source-code-to-spec-documenter/references/source-tracing-rules.md` 與 `source-code-to-spec-documenter/references/generation-handoff-contract.md` |
87
+
88
+ 若一條規則會影響多份 guideline,應在 owner 中維護完整規則,其他文件只保留簡短 cross-reference。
89
+
90
+ ## Guideline Maintenance Workflow
91
+
92
+ 1. 先檢查目前 repo 實際存在的檔案,不要加入未經確認的 phantom references。
93
+ 2. 判斷變更屬於哪個 owner guideline;如果規則已存在,優先原地優化,不新增重複章節。
94
+ 3. 保持 `SKILL.md` 精簡,只放 trigger、routing、owner boundary、workflow 與 quality gate;詳細寫作規則放到 reference guideline。
95
+ 4. 修改 guideline 時,保留正式產出章節與 guideline-only 說明的分界。不要讓「欄位說明」、「撰寫要求」、「圖表規則」被誤產到最終文件。
96
+ 5. 保留 Markdown table、code fence、Mermaid fence、placeholder、technical identifier 與 machine-readable syntax。
97
+ 6. 若新增圖表、流程或範例規則,確認每張圖表後都有結構化文字說明要求。
98
+ 7. 若來源不足,guideline 應要求標示 `N/A`、`無法由目前來源確認`、`unknown` 或 `Unresolved`,不得鼓勵 AI 補完不存在的行為。
99
+ 8. 不要把歷史範例中的示意 table、狀態值、API、Job 或 DB object 當成真實 source evidence;只能作為格式與顆粒度範例。
100
+
101
+ ## Diagram and Metadata Rules
102
+
103
+ 圖表與 Metadata 的補充規則放在 `references/supporting-output-format-diagram-and-example-reference.md`。維護任一 guideline 時,至少要遵守以下共通規則:
104
+
105
+ - 每張 Mermaid、ASCII Art、ERD、UML、flowchart、sequenceDiagram、classDiagram 或其他圖表後面,必須立即附上 `> 📝 **圖表文字說明**` blockquote。
106
+ - 圖表文字說明必須可獨立閱讀;即使 diagram 無法 render,讀者仍能理解架構、流程、資料關聯、分支結果與維運線索。
107
+ - 圖表負責視覺化,圖表文字說明負責補足完整脈絡;兩者共同構成必填內容。
108
+ - Mermaid `sequenceDiagram` 失敗或例外訊息優先使用 `-x`,避免使用不穩定的 `A--xB` 寫法。
109
+ - Mermaid `erDiagram` 的 PK/FK 標示必須使用雙引號,例如 `string PRODUCT_ID "PK"`。
110
+ - Metadata 與 `## Obsidian Links` 規則以 `references/supporting-output-format-diagram-and-example-reference.md` 為主;若任務明確要求「系統功能規格書」格式,使用同一 supporting reference 的 Metadata variant。
111
+
112
+ ## Quality Gate
113
+
114
+ 完成變更前檢查:
115
+
116
+ - `SKILL.md` frontmatter 只有有效的 `name` 與 `description`。
117
+ - `SKILL.md` 沒有引用不存在的 reference、prompt、agent 或 generated output。
118
+ - 每條規則落在正確 owner guideline,沒有在多處複製成 drift 來源。
119
+ - 三份主要 guideline 的使用順序仍清楚:supporting document foundation → foundation/business → architecture/flow/data/logic → error/ops/scenario coverage。
120
+ - 圖表規則仍要求 diagram 後方緊接結構化文字說明。
121
+ - Detail policy 仍要求 source-backed full detail;摘要、圖表或 cross-reference 沒有取代正式詳細章節。
122
+ - File I/O / Report evidence 的欄位主規格由 `02` 的 `6.5 檔案 I/O 規格` 擁有;`03` 只補維運覆蓋,不得取代 `02` 的欄位表。
123
+ - Metadata 規則沒有混淆 guideline 文件格式與 final SPEC 文件格式。
124
+ - Source evidence discipline 由 handoff 承接,target-facing guideline 只要求必要內容與維運判斷方式。
125
+ - 繁體中文敘述與必要 English technical identifiers 都被保留。
126
+ - Markdown tables、code fences、Mermaid syntax、links 與 placeholders 仍可讀且未被翻譯破壞。
127
+
128
+ 若 validator 可用,針對 skill 目錄執行 skill validation;若沒有 validator,至少用 `git diff --check` 檢查 Markdown 空白與格式問題。