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,324 @@
1
+ #!/usr/bin/env python3
2
+ import sys
3
+
4
+ sys.dont_write_bytecode = True
5
+
6
+ import argparse
7
+ import json
8
+ import re
9
+ from pathlib import Path
10
+
11
+
12
+ PREFERRED_SECTIONS = {
13
+ "handoff": "Authoritative Target Source Handoff",
14
+ "direct": "Direct Target Rows",
15
+ "hardcoded": "Hardcoded Target Rows",
16
+ "coverage": "Coverage Review",
17
+ }
18
+
19
+
20
+ def configure_stdio():
21
+ for stream_name in ("stdout", "stderr"):
22
+ stream = getattr(sys, stream_name, None)
23
+ reconfigure = getattr(stream, "reconfigure", None)
24
+ if callable(reconfigure):
25
+ reconfigure(encoding="utf-8")
26
+
27
+
28
+ def normalize_text(value):
29
+ if value is None:
30
+ return ""
31
+ return re.sub(r"\s+", " ", str(value)).strip()
32
+
33
+
34
+ def normalize_key(value):
35
+ return normalize_text(value).casefold()
36
+
37
+
38
+ def split_markdown_row(line):
39
+ line = line.strip()
40
+ if not line.startswith("|") or not line.endswith("|"):
41
+ return []
42
+ cells = []
43
+ current = []
44
+ escaped = False
45
+ for char in line[1:-1]:
46
+ if escaped:
47
+ current.append(char)
48
+ escaped = False
49
+ elif char == "\\":
50
+ escaped = True
51
+ elif char == "|":
52
+ cells.append("".join(current).strip())
53
+ current = []
54
+ else:
55
+ current.append(char)
56
+ cells.append("".join(current).strip())
57
+ return cells
58
+
59
+
60
+ def is_separator(cells):
61
+ return bool(cells) and all(re.fullmatch(r":?-{3,}:?", cell.strip()) for cell in cells)
62
+
63
+
64
+ def markdown_escape(value):
65
+ return normalize_text(value).replace("|", "\\|")
66
+
67
+
68
+ def iter_markdown_tables(lines):
69
+ current_heading = ""
70
+ current_level = 0
71
+ for index, line in enumerate(lines):
72
+ heading = re.match(r"^(#{1,6})\s+(.+?)\s*$", line)
73
+ if heading:
74
+ current_level = len(heading.group(1))
75
+ current_heading = heading.group(2).strip()
76
+ continue
77
+
78
+ header = split_markdown_row(line)
79
+ if not header or index + 1 >= len(lines):
80
+ continue
81
+ separator = split_markdown_row(lines[index + 1])
82
+ if len(separator) != len(header) or not is_separator(separator):
83
+ continue
84
+
85
+ rows = []
86
+ end_line = index + 2
87
+ for data_index in range(index + 2, len(lines)):
88
+ values = split_markdown_row(lines[data_index])
89
+ if len(values) != len(header):
90
+ break
91
+ rows.append(dict(zip(header, values)))
92
+ end_line = data_index + 1
93
+ yield {
94
+ "section": current_heading,
95
+ "section_level": current_level,
96
+ "columns": header,
97
+ "rows": rows,
98
+ "start_line": index + 1,
99
+ "end_line": end_line,
100
+ }
101
+
102
+
103
+ def read_catalog(path):
104
+ path = Path(path)
105
+ lines = path.read_text(encoding="utf-8-sig").splitlines()
106
+ tables = list(iter_markdown_tables(lines))
107
+ if not tables:
108
+ raise SystemExit(f"No Markdown tables found in {path}")
109
+ return {"path": str(path), "tables": tables}
110
+
111
+
112
+ def table_metadata(table):
113
+ return {
114
+ "section": table["section"],
115
+ "section_level": table["section_level"],
116
+ "columns": table["columns"],
117
+ "row_count": len(table["rows"]),
118
+ "start_line": table["start_line"],
119
+ "end_line": table["end_line"],
120
+ }
121
+
122
+
123
+ def section_matches(actual, expected):
124
+ if not expected:
125
+ return True
126
+ return normalize_key(expected) in normalize_key(actual)
127
+
128
+
129
+ def row_matches(row, query=None, column_filters=None):
130
+ if query and normalize_key(query) not in normalize_key(" ".join(row.values())):
131
+ return False
132
+ for column_name, expected in column_filters or []:
133
+ matched_key = None
134
+ for key in row.keys():
135
+ if normalize_key(key) == normalize_key(column_name):
136
+ matched_key = key
137
+ break
138
+ if matched_key is None:
139
+ return False
140
+ if normalize_key(expected) not in normalize_key(row.get(matched_key)):
141
+ return False
142
+ return True
143
+
144
+
145
+ def parse_column_filters(values):
146
+ filters = []
147
+ for value in values or []:
148
+ if "=" not in value:
149
+ raise SystemExit(f"Invalid --where value: {value}. Expected column=value.")
150
+ column_name, expected = value.split("=", 1)
151
+ column_name = normalize_text(column_name)
152
+ expected = normalize_text(expected)
153
+ if not column_name:
154
+ raise SystemExit(f"Invalid --where value: {value}. Missing column name.")
155
+ filters.append((column_name, expected))
156
+ return filters
157
+
158
+
159
+ def collect_rows(catalog, section=None, query=None, column_filters=None, limit=None):
160
+ results = []
161
+ for table in catalog["tables"]:
162
+ if not section_matches(table["section"], section):
163
+ continue
164
+ for row in table["rows"]:
165
+ if not row_matches(row, query=query, column_filters=column_filters):
166
+ continue
167
+ item = dict(row)
168
+ item["_section"] = table["section"]
169
+ item["_line"] = table["start_line"]
170
+ results.append(item)
171
+ if limit is not None and len(results) >= limit:
172
+ return results
173
+ return results
174
+
175
+
176
+ def render_table(rows):
177
+ if not rows:
178
+ return "(no rows)"
179
+ columns = list(rows[0].keys())
180
+ for row in rows[1:]:
181
+ for column in row.keys():
182
+ if column not in columns:
183
+ columns.append(column)
184
+ lines = []
185
+ lines.append("| " + " | ".join(columns) + " |")
186
+ lines.append("| " + " | ".join("---" for _ in columns) + " |")
187
+ for row in rows:
188
+ lines.append("| " + " | ".join(markdown_escape(row.get(column, "")) for column in columns) + " |")
189
+ return "\n".join(lines)
190
+
191
+
192
+ def print_output(payload, output_format):
193
+ if output_format == "json":
194
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
195
+ return
196
+ if isinstance(payload, dict) and "rows" in payload:
197
+ print(render_table(payload["rows"]))
198
+ return
199
+ if isinstance(payload, dict) and "tables" in payload:
200
+ print(render_table(payload["tables"]))
201
+ return
202
+ if isinstance(payload, list):
203
+ print(render_table(payload))
204
+ return
205
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
206
+
207
+
208
+ def command_tables(args):
209
+ catalog = read_catalog(args.catalog)
210
+ tables = [table_metadata(table) for table in catalog["tables"]]
211
+ if args.section:
212
+ tables = [table for table in tables if section_matches(table["section"], args.section)]
213
+ print_output({"catalog": catalog["path"], "count": len(tables), "tables": tables}, args.output)
214
+
215
+
216
+ def command_rows(args):
217
+ catalog = read_catalog(args.catalog)
218
+ rows = collect_rows(
219
+ catalog,
220
+ section=args.section,
221
+ query=args.query,
222
+ column_filters=parse_column_filters(args.where),
223
+ limit=args.limit,
224
+ )
225
+ print_output({"catalog": catalog["path"], "count": len(rows), "rows": rows}, args.output)
226
+
227
+
228
+ def command_handoff(args):
229
+ catalog = read_catalog(args.catalog)
230
+ filters = parse_column_filters(args.where)
231
+ if args.action:
232
+ filters.append(("target_queue_action", args.action))
233
+ if args.target_scope:
234
+ filters.append(("target_scope", args.target_scope))
235
+ if args.authority:
236
+ filters.append(("final_row_authority", args.authority))
237
+ rows = collect_rows(
238
+ catalog,
239
+ section=PREFERRED_SECTIONS["handoff"],
240
+ query=args.query,
241
+ column_filters=filters,
242
+ limit=args.limit,
243
+ )
244
+ print_output({"catalog": catalog["path"], "count": len(rows), "rows": rows}, args.output)
245
+
246
+
247
+ def command_coverage(args):
248
+ catalog = read_catalog(args.catalog)
249
+ rows = collect_rows(
250
+ catalog,
251
+ section=PREFERRED_SECTIONS["coverage"],
252
+ query=args.query,
253
+ column_filters=parse_column_filters(args.where),
254
+ limit=args.limit,
255
+ )
256
+ print_output({"catalog": catalog["path"], "count": len(rows), "rows": rows}, args.output)
257
+
258
+
259
+ def command_search(args):
260
+ catalog = read_catalog(args.catalog)
261
+ rows = collect_rows(catalog, query=args.query, limit=args.limit)
262
+ print_output({"catalog": catalog["path"], "count": len(rows), "rows": rows}, args.output)
263
+
264
+
265
+ def build_parser():
266
+ parser = argparse.ArgumentParser(
267
+ description="Parse and query docs-target-catalog Markdown tables as reusable source handoff data."
268
+ )
269
+ subparsers = parser.add_subparsers(dest="command", required=True)
270
+
271
+ tables = subparsers.add_parser("tables", help="List Markdown tables with section, columns, row count, and lines.")
272
+ tables.add_argument("--catalog", default="docs/docs-target-catalog.md")
273
+ tables.add_argument("--section")
274
+ tables.add_argument("--output", choices=("json", "markdown"), default="json")
275
+ tables.set_defaults(func=command_tables)
276
+
277
+ rows = subparsers.add_parser("rows", help="Query rows from a named catalog section.")
278
+ rows.add_argument("--catalog", default="docs/docs-target-catalog.md")
279
+ rows.add_argument("--section", required=True)
280
+ rows.add_argument("--query")
281
+ rows.add_argument("--where", action="append", default=[], help="Column contains filter in column=value form.")
282
+ rows.add_argument("--limit", type=int)
283
+ rows.add_argument("--output", choices=("json", "markdown"), default="json")
284
+ rows.set_defaults(func=command_rows)
285
+
286
+ handoff = subparsers.add_parser("handoff", help="Query Authoritative Target Source Handoff rows.")
287
+ handoff.add_argument("--catalog", default="docs/docs-target-catalog.md")
288
+ handoff.add_argument("--query")
289
+ handoff.add_argument("--action", help="Filter target_queue_action, e.g. direct_rows or query/export.")
290
+ handoff.add_argument("--target-scope")
291
+ handoff.add_argument("--authority", help="Filter final_row_authority.")
292
+ handoff.add_argument("--where", action="append", default=[], help="Column contains filter in column=value form.")
293
+ handoff.add_argument("--limit", type=int)
294
+ handoff.add_argument("--output", choices=("json", "markdown"), default="json")
295
+ handoff.set_defaults(func=command_handoff)
296
+
297
+ coverage = subparsers.add_parser("coverage", help="Query catalog Coverage Review rows.")
298
+ coverage.add_argument("--catalog", default="docs/docs-target-catalog.md")
299
+ coverage.add_argument("--query")
300
+ coverage.add_argument("--where", action="append", default=[], help="Column contains filter in column=value form.")
301
+ coverage.add_argument("--limit", type=int)
302
+ coverage.add_argument("--output", choices=("json", "markdown"), default="json")
303
+ coverage.set_defaults(func=command_coverage)
304
+
305
+ search = subparsers.add_parser("search", help="Search every parsed catalog table row.")
306
+ search.add_argument("--catalog", default="docs/docs-target-catalog.md")
307
+ search.add_argument("--query", required=True)
308
+ search.add_argument("--limit", type=int)
309
+ search.add_argument("--output", choices=("json", "markdown"), default="json")
310
+ search.set_defaults(func=command_search)
311
+ return parser
312
+
313
+
314
+ def main():
315
+ configure_stdio()
316
+ args = build_parser().parse_args()
317
+ args.func(args)
318
+
319
+
320
+ if __name__ == "__main__":
321
+ try:
322
+ main()
323
+ except BrokenPipeError:
324
+ sys.exit(1)
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env python3
2
+ """Shared documentation target queue schema and normalization helpers."""
3
+
4
+ import sys
5
+
6
+ sys.dont_write_bytecode = True
7
+
8
+ import re
9
+
10
+
11
+ MAIN_COLUMNS = [
12
+ "id",
13
+ "source_type",
14
+ "original_id",
15
+ "group",
16
+ "name",
17
+ "entrypoint",
18
+ "document_path",
19
+ "keyword",
20
+ "doc_profile",
21
+ "foundation_doc_status",
22
+ "architecture_doc_status",
23
+ "ops_doc_status",
24
+ "review_status",
25
+ "document_completed_flag(Y/N)",
26
+ "last_handoff",
27
+ "notes",
28
+ ]
29
+
30
+ SUMMARY_COLUMNS = [
31
+ "target_scope",
32
+ "source_kind",
33
+ "authoritative_source",
34
+ "acquisition_method",
35
+ "selected_fields",
36
+ "filter_or_scope",
37
+ "raw_count",
38
+ "eligible_count",
39
+ "excluded_count",
40
+ "gap_count",
41
+ "validation_status",
42
+ ]
43
+
44
+ SUMMARY_SCOPE_COLUMNS = [
45
+ "target_scope",
46
+ "source_kind",
47
+ ]
48
+
49
+ COVERAGE_COLUMNS = [
50
+ "item",
51
+ "reason_not_in_table",
52
+ "evidence",
53
+ "recommended_next_step",
54
+ ]
55
+
56
+ DOC_STATUSES = {"pending", "in_progress", "generated", "partial", "blocked", "failed", "n/a"}
57
+ FOUNDATION_DOC_STATUSES = DOC_STATUSES
58
+ ARCHITECTURE_DOC_STATUSES = DOC_STATUSES
59
+ OPS_DOC_STATUSES = DOC_STATUSES
60
+ REVIEW_STATUSES = {"not_started", "pending", "passed", "failed", "blocked", "waived"}
61
+ DOC_PROFILES = {"lite", "standard", "full"}
62
+
63
+ SOURCE_TYPE_REGISTRY = {
64
+ "Function": {
65
+ "prefix": "F",
66
+ "path_pattern": "docs/feature/{group}/{name}",
67
+ "default_doc_profile": "standard",
68
+ "scope_hints": ["function", "menu", "navigation", "screen", "ui"],
69
+ "use_for": "user-facing menu/navigation/business capabilities that are not better represented by a specific channel",
70
+ },
71
+ "Job": {
72
+ "prefix": "J",
73
+ "path_pattern": "docs/job/{name}",
74
+ "default_doc_profile": "standard",
75
+ "scope_hints": ["job", "batch", "scheduler", "worker", "cron"],
76
+ "use_for": "scheduled jobs, workers, batch jobs, DB jobs, background processors",
77
+ },
78
+ "API": {
79
+ "prefix": "A",
80
+ "path_pattern": "docs/api/{name}",
81
+ "default_doc_profile": "lite",
82
+ "scope_hints": ["api", "endpoint", "route", "servlet", "service"],
83
+ "use_for": "independently activated REST/SOAP/RPC/service endpoints or service operations",
84
+ },
85
+ "Report": {
86
+ "prefix": "R",
87
+ "path_pattern": "docs/report/{group}/{name}",
88
+ "default_doc_profile": "lite",
89
+ "scope_hints": ["report", "download", "export"],
90
+ "use_for": "independent reports, exports, dashboards, report scheduler identities",
91
+ },
92
+ "File": {
93
+ "prefix": "FI",
94
+ "path_pattern": "docs/file-process/{name}",
95
+ "default_doc_profile": "standard",
96
+ "scope_hints": ["file", "import", "export", "watcher", "etl"],
97
+ "use_for": "file import, export, watcher, transfer, archive processes",
98
+ },
99
+ "External": {
100
+ "prefix": "X",
101
+ "path_pattern": "docs/external/{name}",
102
+ "default_doc_profile": "standard",
103
+ "scope_hints": ["external", "integration", "webhook", "partner"],
104
+ "use_for": "external integrations, webhooks, partner/system interfaces, message contracts",
105
+ },
106
+ "ERP": {
107
+ "prefix": "E",
108
+ "path_pattern": "docs/erp/{group}/{name}",
109
+ "default_doc_profile": "standard",
110
+ "scope_hints": ["erp", "program", "responsibility", "form", "transaction"],
111
+ "use_for": "ERP or platform metadata, forms, responsibilities, programs, transactions",
112
+ },
113
+ "DatabaseProgram": {
114
+ "prefix": "DBP",
115
+ "path_pattern": "docs/database-program/{group}/{name}",
116
+ "default_doc_profile": "standard",
117
+ "scope_hints": [
118
+ "database program",
119
+ "stored procedure",
120
+ "stored function",
121
+ "routine",
122
+ "package",
123
+ "trigger",
124
+ "plsql",
125
+ ],
126
+ "use_for": "database-side packages, stored procedures/functions, triggers, routine tasks, and DB-owned program units",
127
+ },
128
+ "Command": {
129
+ "prefix": "C",
130
+ "path_pattern": "docs/command/{name}",
131
+ "default_doc_profile": "lite",
132
+ "scope_hints": ["command", "cli", "console", "script", "admin"],
133
+ "use_for": "CLI/operator commands, console tasks, scripted commands, admin commands",
134
+ },
135
+ "Workflow": {
136
+ "prefix": "W",
137
+ "path_pattern": "docs/workflow/{name}",
138
+ "default_doc_profile": "standard",
139
+ "scope_hints": ["workflow", "bpmn", "approval", "state", "process"],
140
+ "use_for": "BPMN/process definitions, approval flows, state-machine workflows, orchestrated business processes",
141
+ },
142
+ "MobileScreen": {
143
+ "prefix": "M",
144
+ "path_pattern": "docs/mobile/{group}/{name}",
145
+ "default_doc_profile": "standard",
146
+ "scope_hints": ["mobile", "screen", "tab", "activity", "fragment", "deep link"],
147
+ "use_for": "mobile screens, tabs, activities/fragments, deep links, mobile navigation destinations",
148
+ },
149
+ "DesktopAction": {
150
+ "prefix": "D",
151
+ "path_pattern": "docs/desktop/{group}/{name}",
152
+ "default_doc_profile": "standard",
153
+ "scope_hints": ["desktop", "menu", "toolbar", "shortcut", "window", "form"],
154
+ "use_for": "desktop menu/toolbar actions, shortcut commands, form/window actions, command bindings",
155
+ },
156
+ "LibraryAPI": {
157
+ "prefix": "L",
158
+ "path_pattern": "docs/library-api/{name}",
159
+ "default_doc_profile": "lite",
160
+ "scope_hints": ["library", "sdk", "package", "export", "module"],
161
+ "use_for": "public library/package APIs, SDK entrypoints, exported module contracts",
162
+ },
163
+ "DataPipeline": {
164
+ "prefix": "DP",
165
+ "path_pattern": "docs/data-pipeline/{name}",
166
+ "default_doc_profile": "standard",
167
+ "scope_hints": ["data", "pipeline", "dag", "etl", "elt", "stream"],
168
+ "use_for": "DAGs, ETL/ELT pipelines, stream processors, data product refreshes",
169
+ },
170
+ }
171
+
172
+ DEFAULT_PREFIXES = {source_type: values["prefix"] for source_type, values in SOURCE_TYPE_REGISTRY.items()}
173
+ DEFAULT_PATH_PATTERNS = {
174
+ source_type: values["path_pattern"] for source_type, values in SOURCE_TYPE_REGISTRY.items()
175
+ }
176
+ DEFAULT_DOC_PROFILE_BY_SOURCE_TYPE = {
177
+ source_type: values["default_doc_profile"]
178
+ for source_type, values in SOURCE_TYPE_REGISTRY.items()
179
+ if values["default_doc_profile"] != "standard"
180
+ }
181
+ SOURCE_TYPE_SCOPE_HINTS = {
182
+ source_type.casefold(): values["scope_hints"] for source_type, values in SOURCE_TYPE_REGISTRY.items()
183
+ }
184
+
185
+
186
+ def normalize_text(value):
187
+ if value is None:
188
+ return ""
189
+ return re.sub(r"\s+", " ", str(value)).strip()
190
+
191
+
192
+ def normalize_key(value):
193
+ return normalize_text(value).casefold()
194
+
195
+
196
+ def normalize_status(value, allowed, default):
197
+ value = normalize_text(value).lower()
198
+ return value if value in allowed else default
199
+
200
+
201
+ def normalize_completed_flag(value):
202
+ value = normalize_text(value).upper()
203
+ return value if value in {"Y", "N"} else "N"
204
+
205
+
206
+ def normalize_doc_profile(value, source_type=""):
207
+ value = normalize_text(value).lower()
208
+ if value in DOC_PROFILES:
209
+ return value
210
+ return DEFAULT_DOC_PROFILE_BY_SOURCE_TYPE.get(normalize_text(source_type), "standard")