ai-docs-toolkit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. ai_docs_toolkit/__init__.py +3 -0
  2. ai_docs_toolkit/__main__.py +4 -0
  3. ai_docs_toolkit/cli/__init__.py +1153 -0
  4. ai_docs_toolkit/context/__init__.py +13 -0
  5. ai_docs_toolkit/context/markdown_output.py +470 -0
  6. ai_docs_toolkit/core/__init__.py +360 -0
  7. ai_docs_toolkit/core/azure_devops_provider.py +569 -0
  8. ai_docs_toolkit/core/changed_files.py +235 -0
  9. ai_docs_toolkit/core/config.py +515 -0
  10. ai_docs_toolkit/core/context.py +666 -0
  11. ai_docs_toolkit/core/doctor.py +362 -0
  12. ai_docs_toolkit/core/file_lookup.py +656 -0
  13. ai_docs_toolkit/core/frontmatter.py +143 -0
  14. ai_docs_toolkit/core/graph.py +153 -0
  15. ai_docs_toolkit/core/impact.py +291 -0
  16. ai_docs_toolkit/core/init.py +837 -0
  17. ai_docs_toolkit/core/planning_operations.py +792 -0
  18. ai_docs_toolkit/core/planning_scaffold.py +334 -0
  19. ai_docs_toolkit/core/registry.py +161 -0
  20. ai_docs_toolkit/core/requirement_capture.py +152 -0
  21. ai_docs_toolkit/core/scanner.py +38 -0
  22. ai_docs_toolkit/core/schema_validation.py +222 -0
  23. ai_docs_toolkit/core/section_fix.py +169 -0
  24. ai_docs_toolkit/core/structure_validation.py +1155 -0
  25. ai_docs_toolkit/core/task_export.py +639 -0
  26. ai_docs_toolkit/docs/agent-installation.md +443 -0
  27. ai_docs_toolkit/graph/__init__.py +13 -0
  28. ai_docs_toolkit/graph/json_output.py +238 -0
  29. ai_docs_toolkit/impact/__init__.py +15 -0
  30. ai_docs_toolkit/impact/json_output.py +458 -0
  31. ai_docs_toolkit/mcp/__init__.py +8 -0
  32. ai_docs_toolkit/mcp/__main__.py +5 -0
  33. ai_docs_toolkit/mcp/server.py +345 -0
  34. ai_docs_toolkit/mcp/setup.py +107 -0
  35. ai_docs_toolkit/templates/project/.ai-docs/examples/skills/release-publication/SKILL.md +85 -0
  36. ai_docs_toolkit/templates/project/.ai-docs/reference/documentation-cascade-authoring-skill.md +157 -0
  37. ai_docs_toolkit/templates/project/.ai-docs/reference/documentation-change-review-skill.md +128 -0
  38. ai_docs_toolkit/templates/project/.ai-docs/reference/incoming-request-model.md +332 -0
  39. ai_docs_toolkit/templates/project/.ai-docs/reference/incoming-request-triage-skill.md +158 -0
  40. ai_docs_toolkit/templates/project/.ai-docs/reference/init-command-contract.md +262 -0
  41. ai_docs_toolkit/templates/project/.ai-docs/reference/planning-document-model.md +1483 -0
  42. ai_docs_toolkit/templates/project/.ai-docs/reference/toolkit-maintenance-context-routing.md +373 -0
  43. ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/acceptance.schema.yaml +17 -0
  44. ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/architecture-decision.schema.yaml +17 -0
  45. ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/asset.schema.yaml +17 -0
  46. ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/business-rule.schema.yaml +17 -0
  47. ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/contract.schema.yaml +17 -0
  48. ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/guide.schema.yaml +17 -0
  49. ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/implementation-task.schema.yaml +97 -0
  50. ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/index.schema.yaml +17 -0
  51. ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/inventory.schema.yaml +17 -0
  52. ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/module.schema.yaml +17 -0
  53. ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/operation.schema.yaml +17 -0
  54. ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/plan-stage.schema.yaml +89 -0
  55. ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/plan.schema.yaml +86 -0
  56. ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/runbook.schema.yaml +17 -0
  57. ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/scenario.schema.yaml +17 -0
  58. ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/standalone-task.schema.yaml +90 -0
  59. ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/task-registry.schema.yaml +114 -0
  60. ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/test-case.schema.yaml +23 -0
  61. ai_docs_toolkit/templates/project/.ai-docs/schemas/frontmatter.schema.yaml +181 -0
  62. ai_docs_toolkit/templates/project/.ai-docs/skills/documentation-cascade-authoring/SKILL.md +84 -0
  63. ai_docs_toolkit/templates/project/.ai-docs/skills/documentation-change-review/SKILL.md +76 -0
  64. ai_docs_toolkit/templates/project/.ai-docs/skills/incoming-request-triage/SKILL.md +90 -0
  65. ai_docs_toolkit/templates/project/.ai-docs/skills/index.md +51 -0
  66. ai_docs_toolkit/templates/project/.ai-docs/skills/planning-record-generation/SKILL.md +77 -0
  67. ai_docs_toolkit/templates/project/.ai-docs/skills/planning-task-execution/SKILL.md +106 -0
  68. ai_docs_toolkit/templates/project/.ai-docs/skills/toolkit-consumer-policy/SKILL.md +44 -0
  69. ai_docs_toolkit/templates/project/.ai-docs/skills/update-ai-docs-toolkit/SKILL.md +100 -0
  70. ai_docs_toolkit/templates/project/.ai-docs/templates/release/agent-context-architecture-beta-18.md +123 -0
  71. ai_docs_toolkit/templates/project/.ai-docs/templates/release/archive-validation-beta-12.md +123 -0
  72. ai_docs_toolkit/templates/project/.ai-docs/templates/release/cascade-impact-beta-14.md +69 -0
  73. ai_docs_toolkit/templates/project/.ai-docs/templates/release/consumer-migration-hardening.md +128 -0
  74. ai_docs_toolkit/templates/project/.ai-docs/templates/release/decision-visibility-beta-13.md +132 -0
  75. ai_docs_toolkit/templates/project/.ai-docs/templates/release/doctor-filesystem-traversal-beta-19.md +98 -0
  76. ai_docs_toolkit/templates/project/.ai-docs/templates/release/doctor-json-provenance-beta-20.md +107 -0
  77. ai_docs_toolkit/templates/project/.ai-docs/templates/release/documentation-cascade-authoring-beta-23.md +54 -0
  78. ai_docs_toolkit/templates/project/.ai-docs/templates/release/documentation-change-review-beta-21.md +105 -0
  79. ai_docs_toolkit/templates/project/.ai-docs/templates/release/incoming-request-storage-beta-17.md +70 -0
  80. ai_docs_toolkit/templates/project/.ai-docs/templates/release/incoming-request-triage-beta-22.md +52 -0
  81. ai_docs_toolkit/templates/project/.ai-docs/templates/release/migration-note.md +90 -0
  82. ai_docs_toolkit/templates/project/.ai-docs/templates/release/post-publication-checks.md +9 -0
  83. ai_docs_toolkit/templates/project/.ai-docs/templates/release/release-task.md +123 -0
  84. ai_docs_toolkit/templates/project/.ai-docs/templates/release/routing-restoration-beta-15.md +39 -0
  85. ai_docs_toolkit/templates/project/.ai-docs/templates/release/task-retirement-beta-16.md +54 -0
  86. ai_docs_toolkit/templates/project/.ai-docs/templates/release/validation-evidence.md +18 -0
  87. ai_docs_toolkit/templates/project/.ai-docs/templates/release/version-change-checklist.md +11 -0
  88. ai_docs_toolkit/templates/project/.ai-docs/workflows/documentation-cascade-adoption.md +581 -0
  89. ai_docs_toolkit/templates/project/.ai-docs/workflows/plan-stage-workflow.md +752 -0
  90. ai_docs_toolkit/templates/project/.ai-docs/workflows/toolkit-migration-workflow.md +150 -0
  91. ai_docs_toolkit/templates/project/AGENTS.md +177 -0
  92. ai_docs_toolkit/templates/project/ai-docs-index.md +39 -0
  93. ai_docs_toolkit/templates/project/ai-docs.config.yaml +56 -0
  94. ai_docs_toolkit/validation/__init__.py +16 -0
  95. ai_docs_toolkit/validation/human.py +313 -0
  96. ai_docs_toolkit-0.1.0.dist-info/METADATA +269 -0
  97. ai_docs_toolkit-0.1.0.dist-info/RECORD +100 -0
  98. ai_docs_toolkit-0.1.0.dist-info/WHEEL +5 -0
  99. ai_docs_toolkit-0.1.0.dist-info/entry_points.txt +3 -0
  100. ai_docs_toolkit-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1153 @@
1
+ from ai_docs_toolkit import __version__
2
+ from ai_docs_toolkit.context import (
3
+ context_project,
4
+ format_context_json,
5
+ format_context_markdown,
6
+ )
7
+ from ai_docs_toolkit.graph import GraphFilters, format_graph_json, graph_project
8
+ from ai_docs_toolkit.impact import (
9
+ format_impact_human,
10
+ format_impact_json,
11
+ impact_changed_project,
12
+ impact_project,
13
+ )
14
+ from ai_docs_toolkit.mcp.setup import (
15
+ format_jetbrains_mcp_config_json,
16
+ format_mcp_status_human,
17
+ jetbrains_mcp_config,
18
+ mcp_setup_status,
19
+ )
20
+ from ai_docs_toolkit.core import (
21
+ InitResult,
22
+ AzureDevOpsConfig,
23
+ AzureDevOpsPushResult,
24
+ doctor_project,
25
+ azure_devops_config_from_env,
26
+ format_azure_devops_push_json,
27
+ format_doctor_json,
28
+ format_doctor_human,
29
+ format_file_lookup_human,
30
+ format_file_lookup_json,
31
+ format_planning_operation_human,
32
+ format_planning_operation_json,
33
+ format_planning_layout_migration_dry_run,
34
+ format_planning_scaffold_result,
35
+ format_stage_review_human,
36
+ format_stage_review_json,
37
+ format_task_export_json,
38
+ format_task_export_markdown,
39
+ init_project,
40
+ lookup_document_file,
41
+ lookup_plan_file,
42
+ lookup_request_file,
43
+ lookup_stage_file,
44
+ lookup_task_file,
45
+ planning_layout_migration_dry_run,
46
+ scaffold_planning_record,
47
+ start_stage,
48
+ start_task,
49
+ close_stage,
50
+ close_task,
51
+ add_stage,
52
+ add_task,
53
+ review_stage,
54
+ export_tasks,
55
+ push_azure_devops_tasks,
56
+ autofix_planning,
57
+ fix_sections,
58
+ add_requirement,
59
+ )
60
+ from ai_docs_toolkit.validation import (
61
+ format_human_validation_result,
62
+ format_json_validation_result,
63
+ validate_project_human,
64
+ )
65
+
66
+
67
+ def _help_text() -> str:
68
+ return "\n".join(
69
+ [
70
+ f"ai-docs {__version__}",
71
+ "",
72
+ "Usage:",
73
+ " ai-docs --help",
74
+ " ai-docs --version",
75
+ " ai-docs init [--profile minimal|agent] [--force] [--dry-run|--check]",
76
+ " ai-docs doctor [--format json|--json]",
77
+ " ai-docs file (--id ID | --task TASK | --request REQUEST | --plan PLAN [--stage STAGE]) [--absolute] [--format json|--json]",
78
+ " ai-docs validate",
79
+ " ai-docs validate --json",
80
+ " ai-docs validate --fix-planning [--dry-run] [--format json|--json]",
81
+ " ai-docs fix-sections [--dry-run] [--format json|--json]",
82
+ " ai-docs add-requirement TARGET-ID \"requirement\" [--dry-run] [--format json|--json]",
83
+ " ai-docs plan start-stage --stage STAGE-ID [--dry-run] [--format json|--json]",
84
+ " ai-docs plan start-task --task TASK-ID [--dry-run] [--format json|--json]",
85
+ " ai-docs plan close-task --task TASK-ID [--dry-run] [--format json|--json]",
86
+ " ai-docs plan close-stage --stage STAGE-ID [--dry-run] [--format json|--json]",
87
+ " ai-docs plan add-stage --plan PLAN-ID --stage SLUG [--dry-run] [--format json|--json]",
88
+ " ai-docs plan add-task --stage STAGE-ID --task SLUG [--dry-run] [--format json|--json]",
89
+ " ai-docs plan review-stage --stage STAGE-ID [--format json|--json]",
90
+ " ai-docs tasks export (--task TASK-ID | --stage STAGE-ID | --plan PLAN-ID) [--include-planned] [--include-backlog] [--format json|markdown] [--output PATH]",
91
+ " ai-docs tasks push --provider azure-devops (--task TASK-ID | --stage STAGE-ID | --plan PLAN-ID) --organization ORG --project PROJECT [--dry-run] [--write-back] [--format json]",
92
+ " ai-docs graph --format json [--type TYPE] [--module MODULE] [--id ID]",
93
+ " ai-docs impact (--id ID | --changed [--base REF]) [--format json]",
94
+ " ai-docs context (--id ID | --module MODULE | --feature FEATURE | --changed) [--format json] [--history exclude|include|legacy] [--body-budget-bytes N] [--depth N]",
95
+ " ai-docs mcp status [--project-root PATH] [--config FILE]",
96
+ " ai-docs mcp configure --client jetbrains --print-json [--project-root PATH] [--config FILE]",
97
+ " ai-docs planning scaffold --plan SLUG --stage SLUG --task SLUG --task-id TASK-NNNNNN [--date YYYY-MM-DD]",
98
+ " ai-docs planning migrate-layout --dry-run",
99
+ ]
100
+ )
101
+
102
+
103
+ def main(argv: list[str] | None = None) -> int:
104
+ args = list(argv) if argv is not None else None
105
+
106
+ if args is None:
107
+ import sys
108
+
109
+ args = sys.argv[1:]
110
+
111
+ if not args or "--help" in args or "-h" in args:
112
+ print(_help_text())
113
+ return 0
114
+
115
+ if "--version" in args or "-V" in args:
116
+ print(__version__)
117
+ return 0
118
+
119
+ init_args = _parse_init_args(args)
120
+ if init_args is not None:
121
+ profile, force, dry_run, check = init_args
122
+ result = init_project(
123
+ ".",
124
+ profile=profile,
125
+ force=force,
126
+ dry_run=dry_run,
127
+ check=check,
128
+ )
129
+ print(_format_init_result(result))
130
+ return result.exit_code
131
+
132
+ doctor_format = _parse_doctor_args(args)
133
+ if doctor_format is not None:
134
+ result = doctor_project(".")
135
+ print(
136
+ format_doctor_json(result)
137
+ if doctor_format == "json"
138
+ else format_doctor_human(result)
139
+ )
140
+ return result.exit_code
141
+
142
+ file_args = _parse_file_args(args)
143
+ if file_args is not None:
144
+ selector, value, stage_value, absolute, output_format = file_args
145
+ if selector == "id":
146
+ result = lookup_document_file(value, ".")
147
+ elif selector == "task":
148
+ result = lookup_task_file(value, ".")
149
+ elif selector == "request":
150
+ result = lookup_request_file(value, ".")
151
+ elif selector == "plan" and stage_value is not None:
152
+ result = lookup_stage_file(value, stage_value, ".")
153
+ else:
154
+ result = lookup_plan_file(value, ".")
155
+ print(
156
+ format_file_lookup_json(result)
157
+ if output_format == "json"
158
+ else format_file_lookup_human(result, absolute=absolute)
159
+ )
160
+ return result.exit_code
161
+
162
+ if args == ["validate"]:
163
+ result = validate_project_human(".")
164
+ print(format_human_validation_result(result))
165
+ return result.exit_code
166
+
167
+ if args == ["validate", "--json"] or args == ["--json", "validate"]:
168
+ result = validate_project_human(".")
169
+ print(format_json_validation_result(result))
170
+ return result.exit_code
171
+
172
+ validate_fix_args = _parse_validate_fix_args(args)
173
+ if validate_fix_args is not None:
174
+ dry_run, output_format = validate_fix_args
175
+ fix_result = autofix_planning(".", dry_run=dry_run)
176
+ if output_format == "json":
177
+ print(format_planning_operation_json(fix_result))
178
+ else:
179
+ print(format_planning_operation_human(fix_result))
180
+ if fix_result.exit_code != 0:
181
+ return fix_result.exit_code
182
+ if dry_run:
183
+ return 0
184
+ validation_result = validate_project_human(".")
185
+ print(format_human_validation_result(validation_result))
186
+ return validation_result.exit_code
187
+
188
+ fix_sections_args = _parse_simple_mutation_args(args, "fix-sections")
189
+ if fix_sections_args is not None:
190
+ dry_run, output_format = fix_sections_args
191
+ result = fix_sections(".", dry_run=dry_run)
192
+ print(
193
+ format_planning_operation_json(result)
194
+ if output_format == "json"
195
+ else format_planning_operation_human(result)
196
+ )
197
+ return result.exit_code
198
+
199
+ add_requirement_args = _parse_add_requirement_args(args)
200
+ if add_requirement_args is not None:
201
+ target_id, requirement, dry_run, output_format = add_requirement_args
202
+ result = add_requirement(
203
+ ".",
204
+ target_id=target_id,
205
+ requirement=requirement,
206
+ dry_run=dry_run,
207
+ )
208
+ print(
209
+ format_planning_operation_json(result)
210
+ if output_format == "json"
211
+ else format_planning_operation_human(result)
212
+ )
213
+ return result.exit_code
214
+
215
+ plan_args = _parse_plan_args(args)
216
+ if plan_args is not None:
217
+ command, target, slug, dry_run, output_format = plan_args
218
+ if command == "start-stage":
219
+ result = start_stage(".", stage_id=target, dry_run=dry_run)
220
+ elif command == "start-task":
221
+ result = start_task(".", task_id=target, dry_run=dry_run)
222
+ elif command == "close-task":
223
+ result = close_task(".", task_id=target, dry_run=dry_run)
224
+ elif command == "close-stage":
225
+ result = close_stage(".", stage_id=target, dry_run=dry_run)
226
+ elif command == "add-stage":
227
+ result = add_stage(".", plan_id=target, stage_slug=slug or "", dry_run=dry_run)
228
+ elif command == "add-task":
229
+ result = add_task(".", stage_id=target, task_slug=slug or "", dry_run=dry_run)
230
+ else:
231
+ review = review_stage(".", stage_id=target)
232
+ print(
233
+ format_stage_review_json(review)
234
+ if output_format == "json"
235
+ else format_stage_review_human(review)
236
+ )
237
+ return review.exit_code
238
+ print(
239
+ format_planning_operation_json(result)
240
+ if output_format == "json"
241
+ else format_planning_operation_human(result)
242
+ )
243
+ return result.exit_code
244
+
245
+ graph_filters = _parse_graph_args(args)
246
+ if graph_filters is not None:
247
+ result = graph_project(".", filters=graph_filters)
248
+ print(format_graph_json(result))
249
+ return result.exit_code
250
+
251
+ impact_args = _parse_impact_args(args)
252
+ if impact_args is not None:
253
+ impact_mode, impact_source_ids, impact_format, impact_base = impact_args
254
+ result = (
255
+ impact_changed_project(".", base_ref=impact_base)
256
+ if impact_mode == "changed"
257
+ else impact_project(impact_source_ids, ".")
258
+ )
259
+ print(
260
+ format_impact_json(result)
261
+ if impact_format == "json"
262
+ else format_impact_human(result)
263
+ )
264
+ return result.exit_code
265
+
266
+ context_args = _parse_context_args(args)
267
+ if context_args is not None:
268
+ context_mode, context_value, context_format, history, budget, depth = context_args
269
+ result = context_project(mode=context_mode, value=context_value, project_root=".", history=history, body_budget_bytes=budget, max_depth=depth)
270
+ print(
271
+ format_context_json(result)
272
+ if context_format == "json"
273
+ else format_context_markdown(result)
274
+ )
275
+ return result.exit_code
276
+
277
+ tasks_export_args = _parse_tasks_export_args(args)
278
+ if tasks_export_args is not None:
279
+ selector, value, include_planned, include_backlog, output_format, output_path = (
280
+ tasks_export_args
281
+ )
282
+ result = export_tasks(
283
+ ".",
284
+ task_id=value if selector == "task" else None,
285
+ stage_id=value if selector == "stage" else None,
286
+ plan_id=value if selector == "plan" else None,
287
+ include_planned=include_planned,
288
+ include_backlog=include_backlog,
289
+ )
290
+ output = (
291
+ format_task_export_markdown(result)
292
+ if output_format == "markdown"
293
+ else format_task_export_json(result)
294
+ )
295
+ if output_path is not None:
296
+ from pathlib import Path
297
+
298
+ Path(output_path).write_text(output, encoding="utf-8")
299
+ else:
300
+ print(output, end="")
301
+ return result.exit_code
302
+
303
+ tasks_push_args = _parse_tasks_push_args(args)
304
+ if tasks_push_args is not None:
305
+ (
306
+ selector,
307
+ value,
308
+ provider,
309
+ organization,
310
+ project,
311
+ work_item_type,
312
+ include_planned,
313
+ include_backlog,
314
+ dry_run,
315
+ write_back,
316
+ ) = tasks_push_args
317
+ if provider != "azure-devops":
318
+ import sys
319
+
320
+ print("Unsupported task provider. Supported providers: azure-devops", file=sys.stderr)
321
+ return 3
322
+ config_result = azure_devops_config_from_env(
323
+ organization=organization,
324
+ project=project,
325
+ work_item_type=work_item_type,
326
+ )
327
+ if not isinstance(config_result, AzureDevOpsConfig):
328
+ result = AzureDevOpsPushResult(exit_code=1, issues=(config_result,))
329
+ else:
330
+ result = push_azure_devops_tasks(
331
+ ".",
332
+ config=config_result,
333
+ task_id=value if selector == "task" else None,
334
+ stage_id=value if selector == "stage" else None,
335
+ plan_id=value if selector == "plan" else None,
336
+ include_planned=include_planned,
337
+ include_backlog=include_backlog,
338
+ dry_run=dry_run,
339
+ write_back=write_back,
340
+ )
341
+ print(format_azure_devops_push_json(result), end="")
342
+ return result.exit_code
343
+
344
+ mcp_args = _parse_mcp_args(args)
345
+ if mcp_args is not None:
346
+ mode, project_root, config_file = mcp_args
347
+ if mode == "status":
348
+ result = mcp_setup_status(project_root=project_root, config_file=config_file)
349
+ print(format_mcp_status_human(result))
350
+ return 0 if result["ok"] else 1
351
+
352
+ if mode == "jetbrains-json":
353
+ print(
354
+ format_jetbrains_mcp_config_json(
355
+ jetbrains_mcp_config(
356
+ project_root=project_root,
357
+ config_file=config_file,
358
+ )
359
+ )
360
+ )
361
+ return 0
362
+
363
+ planning_args = _parse_planning_args(args)
364
+ if planning_args is not None:
365
+ if planning_args == ("migrate-layout",):
366
+ result = planning_layout_migration_dry_run(".")
367
+ print(format_planning_layout_migration_dry_run(result))
368
+ return 0
369
+
370
+ plan_slug, stage_slug, task_slug, task_id, plan_date = planning_args
371
+ result = scaffold_planning_record(
372
+ ".",
373
+ plan_slug=plan_slug,
374
+ stage_slug=stage_slug,
375
+ task_slug=task_slug,
376
+ task_id=task_id,
377
+ plan_date=plan_date,
378
+ )
379
+ print(format_planning_scaffold_result(result))
380
+ return result.exit_code
381
+
382
+ if args and args[0] == "graph":
383
+ import sys
384
+
385
+ print("Usage: ai-docs graph --format json", file=sys.stderr)
386
+ return 3
387
+
388
+ if args and args[0] == "impact":
389
+ import sys
390
+
391
+ print(
392
+ "Usage: ai-docs impact (--id ID | --changed [--base REF]) [--format json]",
393
+ file=sys.stderr,
394
+ )
395
+ return 3
396
+
397
+ if args and args[0] == "context":
398
+ import sys
399
+
400
+ print(
401
+ "Usage: ai-docs context (--id ID | --module MODULE | --feature FEATURE | --changed) [--format json] [--history exclude|include|legacy] [--body-budget-bytes N] [--depth N]",
402
+ file=sys.stderr,
403
+ )
404
+ return 3
405
+
406
+ if args and args[0] == "tasks":
407
+ import sys
408
+
409
+ print(
410
+ "Usage: ai-docs tasks export (--task TASK-ID | --stage STAGE-ID | --plan PLAN-ID) "
411
+ "[--include-planned] [--include-backlog] [--format json|markdown] [--output PATH]\n"
412
+ " or: ai-docs tasks push --provider azure-devops (--task TASK-ID | --stage STAGE-ID | --plan PLAN-ID) "
413
+ "--organization ORG --project PROJECT [--dry-run] [--write-back] [--format json]",
414
+ file=sys.stderr,
415
+ )
416
+ return 3
417
+
418
+ if args and args[0] == "init":
419
+ import sys
420
+
421
+ print(
422
+ "Usage: ai-docs init [--profile minimal|agent] [--force] [--dry-run|--check]",
423
+ file=sys.stderr,
424
+ )
425
+ return 3
426
+
427
+ if args and args[0] == "doctor":
428
+ import sys
429
+
430
+ print("Usage: ai-docs doctor [--format json|--json]", file=sys.stderr)
431
+ return 3
432
+
433
+ if args and args[0] == "fix-sections":
434
+ import sys
435
+
436
+ print("Usage: ai-docs fix-sections [--dry-run] [--format json|--json]", file=sys.stderr)
437
+ return 3
438
+
439
+ if args and args[0] == "add-requirement":
440
+ import sys
441
+
442
+ print(
443
+ "Usage: ai-docs add-requirement TARGET-ID \"requirement\" [--dry-run] [--format json|--json]",
444
+ file=sys.stderr,
445
+ )
446
+ return 3
447
+
448
+ if args and args[0] == "file":
449
+ import sys
450
+
451
+ print(
452
+ "Usage: ai-docs file (--id ID | --task TASK | --request REQUEST | --plan PLAN [--stage STAGE]) [--absolute] [--format json|--json]",
453
+ file=sys.stderr,
454
+ )
455
+ return 3
456
+
457
+ if args and args[0] == "plan":
458
+ import sys
459
+
460
+ print(
461
+ "Usage: ai-docs plan (start-stage|close-stage) --stage STAGE-ID [--dry-run] [--format json|--json]\n"
462
+ " or: ai-docs plan (start-task|close-task) --task TASK-ID [--dry-run] [--format json|--json]\n"
463
+ " or: ai-docs plan add-stage --plan PLAN-ID --stage SLUG [--dry-run] [--format json|--json]\n"
464
+ " or: ai-docs plan add-task --stage STAGE-ID --task SLUG [--dry-run] [--format json|--json]\n"
465
+ " or: ai-docs plan review-stage --stage STAGE-ID [--format json|--json]",
466
+ file=sys.stderr,
467
+ )
468
+ return 3
469
+
470
+ if args and args[0] == "mcp":
471
+ import sys
472
+
473
+ print(
474
+ "Usage: ai-docs mcp status [--project-root PATH] [--config FILE]\n"
475
+ " or: ai-docs mcp configure --client jetbrains --print-json [--project-root PATH] [--config FILE]",
476
+ file=sys.stderr,
477
+ )
478
+ return 3
479
+
480
+ if args and args[0] == "planning":
481
+ import sys
482
+
483
+ print(
484
+ "Usage: ai-docs planning scaffold --plan SLUG --stage SLUG --task SLUG --task-id TASK-NNNNNN [--date YYYY-MM-DD]\n"
485
+ " or: ai-docs planning migrate-layout --dry-run",
486
+ file=sys.stderr,
487
+ )
488
+ return 3
489
+
490
+ import sys
491
+
492
+ print(f"Unknown command or option: {' '.join(args)}", file=sys.stderr)
493
+ print("Run `ai-docs --help` for usage.", file=sys.stderr)
494
+ return 3
495
+
496
+
497
+ def _parse_init_args(args: list[str]) -> tuple[str, bool, bool, bool] | None:
498
+ if not args or args[0] != "init":
499
+ return None
500
+
501
+ values = args[1:]
502
+ profile = "agent"
503
+ force = False
504
+ dry_run = False
505
+ check = False
506
+ index = 0
507
+ while index < len(values):
508
+ option = values[index]
509
+ if option == "--force":
510
+ force = True
511
+ index += 1
512
+ continue
513
+
514
+ if option == "--dry-run":
515
+ dry_run = True
516
+ index += 1
517
+ continue
518
+
519
+ if option == "--check":
520
+ check = True
521
+ index += 1
522
+ continue
523
+
524
+ if option == "--profile":
525
+ if index + 1 >= len(values):
526
+ return None
527
+ profile = values[index + 1]
528
+ if profile not in {"minimal", "agent"}:
529
+ return None
530
+ index += 2
531
+ continue
532
+
533
+ return None
534
+
535
+ if dry_run and check:
536
+ return None
537
+
538
+ return profile, force, dry_run, check
539
+
540
+
541
+ def _parse_doctor_args(args: list[str]) -> str | None:
542
+ if args == ["doctor"]:
543
+ return "human"
544
+ if args in (["doctor", "--json"], ["--json", "doctor"]):
545
+ return "json"
546
+ if args == ["doctor", "--format", "json"]:
547
+ return "json"
548
+ return None
549
+
550
+
551
+ def _format_init_result(result: InitResult) -> str:
552
+ lines = [
553
+ f"ai-docs init ({result.mode})",
554
+ f"profile: {result.profile}",
555
+ "created:",
556
+ *_format_path_list(result.created),
557
+ "skipped:",
558
+ *_format_path_list(result.skipped),
559
+ ]
560
+
561
+ if result.overwritten:
562
+ lines.extend(["overwritten:", *_format_path_list(result.overwritten)])
563
+
564
+ for label, paths in (
565
+ ("missing", result.missing),
566
+ ("stale-known", result.stale_known),
567
+ ("locally-modified", result.locally_modified),
568
+ ("provenance-missing", result.provenance_missing),
569
+ ):
570
+ if paths:
571
+ lines.extend([f"{label}:", *_format_path_list(paths)])
572
+
573
+ if result.unresolved:
574
+ lines.extend(
575
+ [
576
+ "unresolved (review and rerun with --force to overwrite):",
577
+ *_format_path_list(result.unresolved),
578
+ ]
579
+ )
580
+
581
+ if result.obsolete:
582
+ lines.extend(["obsolete:", *_format_path_list(result.obsolete)])
583
+
584
+ if result.errors:
585
+ lines.extend(["errors:", *_format_path_list(result.errors)])
586
+
587
+ return "\n".join(lines)
588
+
589
+
590
+ def _format_path_list(paths: tuple[str, ...]) -> list[str]:
591
+ if not paths:
592
+ return ["- none"]
593
+ return [f"- {path}" for path in paths]
594
+
595
+
596
+ def _parse_file_args(args: list[str]) -> tuple[str, str, str | None, bool, str] | None:
597
+ if not args or args[0] != "file":
598
+ return None
599
+
600
+ values = args[1:]
601
+ selector: str | None = None
602
+ selector_value: str | None = None
603
+ stage_value: str | None = None
604
+ absolute = False
605
+ output_format = "human"
606
+ index = 0
607
+ while index < len(values):
608
+ option = values[index]
609
+ if option == "--absolute":
610
+ absolute = True
611
+ index += 1
612
+ continue
613
+
614
+ if option == "--json":
615
+ output_format = "json"
616
+ index += 1
617
+ continue
618
+
619
+ if option == "--format":
620
+ if index + 1 >= len(values) or values[index + 1] != "json":
621
+ return None
622
+ output_format = "json"
623
+ index += 2
624
+ continue
625
+
626
+ if option in {"--id", "--task", "--request", "--plan"}:
627
+ if index + 1 >= len(values) or selector is not None:
628
+ return None
629
+ selector = option.removeprefix("--")
630
+ selector_value = values[index + 1]
631
+ index += 2
632
+ continue
633
+
634
+ if option == "--stage":
635
+ if index + 1 >= len(values) or stage_value is not None:
636
+ return None
637
+ stage_value = values[index + 1]
638
+ index += 2
639
+ continue
640
+
641
+ return None
642
+
643
+ if selector is None or selector_value is None:
644
+ return None
645
+
646
+ if stage_value is not None and selector != "plan":
647
+ return None
648
+
649
+ return selector, selector_value, stage_value, absolute, output_format
650
+
651
+
652
+ def _parse_validate_fix_args(args: list[str]) -> tuple[bool, str] | None:
653
+ if not args or args[0] != "validate" or "--fix-planning" not in args:
654
+ return None
655
+
656
+ dry_run = False
657
+ output_format = "human"
658
+ index = 1
659
+ while index < len(args):
660
+ option = args[index]
661
+ if option == "--fix-planning":
662
+ index += 1
663
+ continue
664
+ if option == "--dry-run":
665
+ dry_run = True
666
+ index += 1
667
+ continue
668
+ if option == "--json":
669
+ output_format = "json"
670
+ index += 1
671
+ continue
672
+ if option == "--format":
673
+ if index + 1 >= len(args) or args[index + 1] != "json":
674
+ return None
675
+ output_format = "json"
676
+ index += 2
677
+ continue
678
+ return None
679
+ return dry_run, output_format
680
+
681
+
682
+ def _parse_simple_mutation_args(args: list[str], command: str) -> tuple[bool, str] | None:
683
+ if not args or args[0] != command:
684
+ return None
685
+
686
+ dry_run = False
687
+ output_format = "human"
688
+ index = 1
689
+ while index < len(args):
690
+ option = args[index]
691
+ if option == "--dry-run":
692
+ dry_run = True
693
+ index += 1
694
+ continue
695
+ if option == "--json":
696
+ output_format = "json"
697
+ index += 1
698
+ continue
699
+ if option == "--format":
700
+ if index + 1 >= len(args) or args[index + 1] != "json":
701
+ return None
702
+ output_format = "json"
703
+ index += 2
704
+ continue
705
+ return None
706
+ return dry_run, output_format
707
+
708
+
709
+ def _parse_add_requirement_args(args: list[str]) -> tuple[str, str, bool, str] | None:
710
+ if not args or args[0] != "add-requirement":
711
+ return None
712
+
713
+ positional: list[str] = []
714
+ dry_run = False
715
+ output_format = "human"
716
+ index = 1
717
+ while index < len(args):
718
+ option = args[index]
719
+ if option == "--dry-run":
720
+ dry_run = True
721
+ index += 1
722
+ continue
723
+ if option == "--json":
724
+ output_format = "json"
725
+ index += 1
726
+ continue
727
+ if option == "--format":
728
+ if index + 1 >= len(args) or args[index + 1] != "json":
729
+ return None
730
+ output_format = "json"
731
+ index += 2
732
+ continue
733
+ if option.startswith("--"):
734
+ return None
735
+ positional.append(option)
736
+ index += 1
737
+
738
+ if len(positional) != 2:
739
+ return None
740
+ return positional[0], positional[1], dry_run, output_format
741
+
742
+
743
+ def _parse_plan_args(args: list[str]) -> tuple[str, str, str | None, bool, str] | None:
744
+ if len(args) < 2 or args[0] != "plan":
745
+ return None
746
+
747
+ command = args[1]
748
+ if command not in {
749
+ "start-stage",
750
+ "start-task",
751
+ "close-task",
752
+ "close-stage",
753
+ "add-stage",
754
+ "add-task",
755
+ "review-stage",
756
+ }:
757
+ return None
758
+
759
+ dry_run = False
760
+ output_format = "human"
761
+ target: str | None = None
762
+ slug: str | None = None
763
+ index = 2
764
+ while index < len(args):
765
+ option = args[index]
766
+ if option == "--dry-run":
767
+ dry_run = True
768
+ index += 1
769
+ continue
770
+ if option == "--json":
771
+ output_format = "json"
772
+ index += 1
773
+ continue
774
+ if option == "--format":
775
+ if index + 1 >= len(args) or args[index + 1] != "json":
776
+ return None
777
+ output_format = "json"
778
+ index += 2
779
+ continue
780
+ if option in {"--task", "--stage", "--plan"}:
781
+ if index + 1 >= len(args):
782
+ return None
783
+ value = args[index + 1]
784
+ if command in {"start-task", "close-task"} and option == "--task":
785
+ target = value
786
+ elif command in {"start-stage", "close-stage", "review-stage"} and option == "--stage":
787
+ target = value
788
+ elif command == "add-stage" and option == "--plan":
789
+ target = value
790
+ elif command == "add-task" and option == "--stage":
791
+ target = value
792
+ elif command == "add-stage" and option == "--stage":
793
+ slug = value
794
+ elif command == "add-task" and option == "--task":
795
+ slug = value
796
+ else:
797
+ return None
798
+ index += 2
799
+ continue
800
+ return None
801
+
802
+ if target is None:
803
+ return None
804
+ if command in {"add-stage", "add-task"} and slug is None:
805
+ return None
806
+ if dry_run and command == "review-stage":
807
+ return None
808
+ return command, target, slug, dry_run, output_format
809
+
810
+
811
+ def _parse_graph_args(args: list[str]) -> GraphFilters | None:
812
+ if not args or args[0] != "graph":
813
+ return None
814
+
815
+ values = args[1:]
816
+ if len(values) < 2 or values[:2] != ["--format", "json"]:
817
+ return None
818
+
819
+ document_type: str | None = None
820
+ module: str | None = None
821
+ document_id: str | None = None
822
+ index = 2
823
+ while index < len(values):
824
+ option = values[index]
825
+ if index + 1 >= len(values):
826
+ return None
827
+
828
+ value = values[index + 1]
829
+ if option == "--type":
830
+ document_type = value
831
+ elif option == "--module":
832
+ module = value
833
+ elif option == "--id":
834
+ document_id = value
835
+ else:
836
+ return None
837
+ index += 2
838
+
839
+ return GraphFilters(
840
+ document_type=document_type,
841
+ module=module,
842
+ document_id=document_id,
843
+ )
844
+
845
+
846
+ def _parse_impact_args(
847
+ args: list[str],
848
+ ) -> tuple[str, tuple[str, ...], str, str | None] | None:
849
+ if not args or args[0] != "impact":
850
+ return None
851
+
852
+ values = args[1:]
853
+ mode: str | None = None
854
+ source_ids: tuple[str, ...] = ()
855
+ output_format = "human"
856
+ base_ref: str | None = None
857
+ index = 0
858
+ while index < len(values):
859
+ value = values[index]
860
+ if value == "--changed" and mode is None:
861
+ mode = "changed"
862
+ index += 1
863
+ continue
864
+ if value == "--id" and mode is None and index + 1 < len(values):
865
+ mode = "id"
866
+ source_ids = (values[index + 1],)
867
+ index += 2
868
+ continue
869
+ if value == "--format" and index + 1 < len(values):
870
+ if values[index + 1] != "json" or output_format != "human":
871
+ return None
872
+ output_format = "json"
873
+ index += 2
874
+ continue
875
+ if value == "--base" and index + 1 < len(values) and base_ref is None:
876
+ base_ref = values[index + 1]
877
+ index += 2
878
+ continue
879
+ return None
880
+
881
+ if mode is None or (mode == "id" and base_ref is not None):
882
+ return None
883
+ return (mode, source_ids, output_format, base_ref)
884
+
885
+
886
+ def _parse_context_args(args: list[str]) -> tuple[str, str, str, str | None, int | None, int | None] | None:
887
+ if not args or args[0] != "context":
888
+ return None
889
+ options = {}
890
+ index = 1
891
+ allowed = {"--id", "--module", "--feature", "--changed", "--format", "--history", "--body-budget-bytes", "--depth"}
892
+ while index < len(args):
893
+ option = args[index]
894
+ if option not in allowed or option in options:
895
+ return None
896
+ if option == "--changed":
897
+ options[option] = ""
898
+ index += 1
899
+ continue
900
+ if index + 1 >= len(args) or not args[index + 1] or args[index + 1].startswith("--"):
901
+ return None
902
+ options[option] = args[index + 1]
903
+ index += 2
904
+ sources = [key for key in options if key in {"--id", "--module", "--feature", "--changed"}]
905
+ if len(sources) != 1 or ("--format" in options and options["--format"] != "json"):
906
+ return None
907
+ history = options.get("--history")
908
+ if history is not None and history not in {"exclude", "include", "legacy"}:
909
+ return None
910
+ numeric = {}
911
+ for key in ("--body-budget-bytes", "--depth"):
912
+ value = options.get(key)
913
+ if value is not None:
914
+ if not value.isascii() or not value.isdigit():
915
+ return None
916
+ try:
917
+ numeric[key] = int(value)
918
+ except ValueError:
919
+ return None
920
+ source = sources[0]
921
+ return (source[2:], options[source], options.get("--format", "markdown"), history,
922
+ numeric.get("--body-budget-bytes"), numeric.get("--depth"))
923
+
924
+
925
+ def _parse_tasks_export_args(
926
+ args: list[str],
927
+ ) -> tuple[str, str, bool, bool, str, str | None] | None:
928
+ if len(args) < 3 or args[:2] != ["tasks", "export"]:
929
+ return None
930
+
931
+ selector: str | None = None
932
+ value: str | None = None
933
+ include_planned = False
934
+ include_backlog = False
935
+ output_format = "json"
936
+ output_path: str | None = None
937
+ index = 2
938
+ while index < len(args):
939
+ option = args[index]
940
+ if option == "--include-planned":
941
+ include_planned = True
942
+ index += 1
943
+ continue
944
+ if option == "--include-backlog":
945
+ include_backlog = True
946
+ index += 1
947
+ continue
948
+ if option == "--format":
949
+ if index + 1 >= len(args) or args[index + 1] not in {"json", "markdown"}:
950
+ return None
951
+ output_format = args[index + 1]
952
+ index += 2
953
+ continue
954
+ if option == "--output":
955
+ if index + 1 >= len(args) or output_path is not None:
956
+ return None
957
+ output_path = args[index + 1]
958
+ index += 2
959
+ continue
960
+ if option in {"--task", "--stage", "--plan"}:
961
+ if index + 1 >= len(args) or selector is not None:
962
+ return None
963
+ selector = option.removeprefix("--")
964
+ value = args[index + 1]
965
+ index += 2
966
+ continue
967
+ return None
968
+
969
+ if selector is None or value is None:
970
+ return None
971
+
972
+ return selector, value, include_planned, include_backlog, output_format, output_path
973
+
974
+
975
+ def _parse_tasks_push_args(
976
+ args: list[str],
977
+ ) -> tuple[str, str, str, str | None, str | None, str, bool, bool, bool, bool] | None:
978
+ if len(args) < 4 or args[:2] != ["tasks", "push"]:
979
+ return None
980
+
981
+ selector: str | None = None
982
+ value: str | None = None
983
+ provider: str | None = None
984
+ organization: str | None = None
985
+ project: str | None = None
986
+ work_item_type = "Task"
987
+ include_planned = False
988
+ include_backlog = False
989
+ dry_run = False
990
+ write_back = False
991
+ output_format = "json"
992
+ index = 2
993
+ while index < len(args):
994
+ option = args[index]
995
+ if option == "--include-planned":
996
+ include_planned = True
997
+ index += 1
998
+ continue
999
+ if option == "--include-backlog":
1000
+ include_backlog = True
1001
+ index += 1
1002
+ continue
1003
+ if option == "--dry-run":
1004
+ dry_run = True
1005
+ index += 1
1006
+ continue
1007
+ if option == "--write-back":
1008
+ write_back = True
1009
+ index += 1
1010
+ continue
1011
+ if option == "--format":
1012
+ if index + 1 >= len(args) or args[index + 1] != "json":
1013
+ return None
1014
+ output_format = args[index + 1]
1015
+ index += 2
1016
+ continue
1017
+ if option in {"--task", "--stage", "--plan"}:
1018
+ if index + 1 >= len(args) or selector is not None:
1019
+ return None
1020
+ selector = option.removeprefix("--")
1021
+ value = args[index + 1]
1022
+ index += 2
1023
+ continue
1024
+ if option in {"--provider", "--organization", "--project", "--work-item-type"}:
1025
+ if index + 1 >= len(args):
1026
+ return None
1027
+ option_value = args[index + 1]
1028
+ if option == "--provider":
1029
+ provider = option_value
1030
+ elif option == "--organization":
1031
+ organization = option_value
1032
+ elif option == "--project":
1033
+ project = option_value
1034
+ else:
1035
+ work_item_type = option_value
1036
+ index += 2
1037
+ continue
1038
+ return None
1039
+
1040
+ if selector is None or value is None or provider is None or output_format != "json":
1041
+ return None
1042
+
1043
+ return (
1044
+ selector,
1045
+ value,
1046
+ provider,
1047
+ organization,
1048
+ project,
1049
+ work_item_type,
1050
+ include_planned,
1051
+ include_backlog,
1052
+ dry_run,
1053
+ write_back,
1054
+ )
1055
+
1056
+
1057
+ def _context_mode(option: str) -> str | None:
1058
+ if option == "--id":
1059
+ return "id"
1060
+ if option == "--module":
1061
+ return "module"
1062
+ if option == "--feature":
1063
+ return "feature"
1064
+ return None
1065
+
1066
+
1067
+ def _parse_mcp_args(args: list[str]) -> tuple[str, str, str] | None:
1068
+ if not args or args[0] != "mcp":
1069
+ return None
1070
+
1071
+ values = args[1:]
1072
+ if not values:
1073
+ return None
1074
+
1075
+ if values[0] == "status":
1076
+ options = _parse_project_options(values[1:])
1077
+ if options is None:
1078
+ return None
1079
+ project_root, config_file = options
1080
+ return ("status", project_root, config_file)
1081
+
1082
+ if values[:4] == ["configure", "--client", "jetbrains", "--print-json"]:
1083
+ options = _parse_project_options(values[4:])
1084
+ if options is None:
1085
+ return None
1086
+ project_root, config_file = options
1087
+ return ("jetbrains-json", project_root, config_file)
1088
+
1089
+ return None
1090
+
1091
+
1092
+ def _parse_project_options(values: list[str]) -> tuple[str, str] | None:
1093
+ project_root = "."
1094
+ config_file = "ai-docs.config.yaml"
1095
+ index = 0
1096
+ while index < len(values):
1097
+ option = values[index]
1098
+ if index + 1 >= len(values):
1099
+ return None
1100
+ value = values[index + 1]
1101
+ if option == "--project-root":
1102
+ project_root = value
1103
+ elif option == "--config":
1104
+ config_file = value
1105
+ else:
1106
+ return None
1107
+ index += 2
1108
+ return project_root, config_file
1109
+
1110
+
1111
+ def _parse_planning_args(args: list[str]):
1112
+ if not args or args[0] != "planning":
1113
+ return None
1114
+
1115
+ if args[1:] == ["migrate-layout", "--dry-run"]:
1116
+ return ("migrate-layout",)
1117
+
1118
+ if args[:2] != ["planning", "scaffold"]:
1119
+ return None
1120
+
1121
+ from datetime import date
1122
+
1123
+ values = args[2:]
1124
+ parsed: dict[str, str] = {}
1125
+ index = 0
1126
+ while index < len(values):
1127
+ option = values[index]
1128
+ if index + 1 >= len(values):
1129
+ return None
1130
+ value = values[index + 1]
1131
+ if option not in {"--plan", "--stage", "--task", "--task-id", "--date"}:
1132
+ return None
1133
+ parsed[option] = value
1134
+ index += 2
1135
+
1136
+ required = {"--plan", "--stage", "--task", "--task-id"}
1137
+ if not required.issubset(parsed):
1138
+ return None
1139
+
1140
+ plan_date = None
1141
+ if "--date" in parsed:
1142
+ try:
1143
+ plan_date = date.fromisoformat(parsed["--date"])
1144
+ except ValueError:
1145
+ return None
1146
+
1147
+ return (
1148
+ parsed["--plan"],
1149
+ parsed["--stage"],
1150
+ parsed["--task"],
1151
+ parsed["--task-id"],
1152
+ plan_date,
1153
+ )