tactus 0.33.0__py3-none-any.whl → 0.34.1__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.
- tactus/__init__.py +1 -1
- tactus/adapters/__init__.py +18 -1
- tactus/adapters/broker_log.py +127 -34
- tactus/adapters/channels/__init__.py +153 -0
- tactus/adapters/channels/base.py +174 -0
- tactus/adapters/channels/broker.py +179 -0
- tactus/adapters/channels/cli.py +448 -0
- tactus/adapters/channels/host.py +225 -0
- tactus/adapters/channels/ipc.py +297 -0
- tactus/adapters/channels/sse.py +305 -0
- tactus/adapters/cli_hitl.py +223 -1
- tactus/adapters/control_loop.py +879 -0
- tactus/adapters/file_storage.py +35 -2
- tactus/adapters/ide_log.py +7 -1
- tactus/backends/http_backend.py +0 -1
- tactus/broker/client.py +31 -1
- tactus/broker/server.py +416 -92
- tactus/cli/app.py +270 -7
- tactus/cli/control.py +393 -0
- tactus/core/config_manager.py +33 -6
- tactus/core/dsl_stubs.py +102 -18
- tactus/core/execution_context.py +265 -8
- tactus/core/lua_sandbox.py +8 -9
- tactus/core/registry.py +19 -2
- tactus/core/runtime.py +235 -27
- tactus/docker/Dockerfile.pypi +49 -0
- tactus/docs/__init__.py +33 -0
- tactus/docs/extractor.py +326 -0
- tactus/docs/html_renderer.py +72 -0
- tactus/docs/models.py +121 -0
- tactus/docs/templates/base.html +204 -0
- tactus/docs/templates/index.html +58 -0
- tactus/docs/templates/module.html +96 -0
- tactus/dspy/agent.py +382 -22
- tactus/dspy/broker_lm.py +57 -6
- tactus/dspy/config.py +14 -3
- tactus/dspy/history.py +2 -1
- tactus/dspy/module.py +136 -11
- tactus/dspy/signature.py +0 -1
- tactus/ide/server.py +300 -9
- tactus/primitives/human.py +619 -47
- tactus/primitives/system.py +0 -1
- tactus/protocols/__init__.py +25 -0
- tactus/protocols/control.py +427 -0
- tactus/protocols/notification.py +207 -0
- tactus/sandbox/container_runner.py +79 -11
- tactus/sandbox/docker_manager.py +23 -0
- tactus/sandbox/entrypoint.py +26 -0
- tactus/sandbox/protocol.py +3 -0
- tactus/stdlib/README.md +77 -0
- tactus/stdlib/__init__.py +27 -1
- tactus/stdlib/classify/__init__.py +165 -0
- tactus/stdlib/classify/classify.spec.tac +195 -0
- tactus/stdlib/classify/classify.tac +257 -0
- tactus/stdlib/classify/fuzzy.py +282 -0
- tactus/stdlib/classify/llm.py +319 -0
- tactus/stdlib/classify/primitive.py +287 -0
- tactus/stdlib/core/__init__.py +57 -0
- tactus/stdlib/core/base.py +320 -0
- tactus/stdlib/core/confidence.py +211 -0
- tactus/stdlib/core/models.py +161 -0
- tactus/stdlib/core/retry.py +171 -0
- tactus/stdlib/core/validation.py +274 -0
- tactus/stdlib/extract/__init__.py +125 -0
- tactus/stdlib/extract/llm.py +330 -0
- tactus/stdlib/extract/primitive.py +256 -0
- tactus/stdlib/tac/tactus/classify/base.tac +51 -0
- tactus/stdlib/tac/tactus/classify/fuzzy.tac +87 -0
- tactus/stdlib/tac/tactus/classify/index.md +77 -0
- tactus/stdlib/tac/tactus/classify/init.tac +29 -0
- tactus/stdlib/tac/tactus/classify/llm.tac +150 -0
- tactus/stdlib/tac/tactus/classify.spec.tac +191 -0
- tactus/stdlib/tac/tactus/extract/base.tac +138 -0
- tactus/stdlib/tac/tactus/extract/index.md +96 -0
- tactus/stdlib/tac/tactus/extract/init.tac +27 -0
- tactus/stdlib/tac/tactus/extract/llm.tac +201 -0
- tactus/stdlib/tac/tactus/extract.spec.tac +153 -0
- tactus/stdlib/tac/tactus/generate/base.tac +142 -0
- tactus/stdlib/tac/tactus/generate/index.md +195 -0
- tactus/stdlib/tac/tactus/generate/init.tac +28 -0
- tactus/stdlib/tac/tactus/generate/llm.tac +169 -0
- tactus/stdlib/tac/tactus/generate.spec.tac +210 -0
- tactus/testing/behave_integration.py +171 -7
- tactus/testing/context.py +0 -1
- tactus/testing/evaluation_runner.py +0 -1
- tactus/testing/gherkin_parser.py +0 -1
- tactus/testing/mock_hitl.py +0 -1
- tactus/testing/mock_tools.py +0 -1
- tactus/testing/models.py +0 -1
- tactus/testing/steps/builtin.py +0 -1
- tactus/testing/steps/custom.py +81 -22
- tactus/testing/steps/registry.py +0 -1
- tactus/testing/test_runner.py +7 -1
- tactus/validation/semantic_visitor.py +11 -5
- tactus/validation/validator.py +0 -1
- {tactus-0.33.0.dist-info → tactus-0.34.1.dist-info}/METADATA +14 -2
- {tactus-0.33.0.dist-info → tactus-0.34.1.dist-info}/RECORD +100 -49
- {tactus-0.33.0.dist-info → tactus-0.34.1.dist-info}/WHEEL +0 -0
- {tactus-0.33.0.dist-info → tactus-0.34.1.dist-info}/entry_points.txt +0 -0
- {tactus-0.33.0.dist-info → tactus-0.34.1.dist-info}/licenses/LICENSE +0 -0
tactus/docs/extractor.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Extract documentation from Tactus .tac files.
|
|
3
|
+
|
|
4
|
+
This module parses .tac files to extract:
|
|
5
|
+
- --[[doc]] comment blocks (Markdown documentation)
|
|
6
|
+
- --[[doc:parameter name]] blocks (Parameter documentation)
|
|
7
|
+
- Specification([[...]]) blocks (BDD specifications in Gherkin format)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List, Optional
|
|
13
|
+
from tactus.docs.models import (
|
|
14
|
+
DocBlock,
|
|
15
|
+
ParameterDoc,
|
|
16
|
+
BDDStep,
|
|
17
|
+
BDDScenario,
|
|
18
|
+
BDDFeature,
|
|
19
|
+
CodeExample,
|
|
20
|
+
ModuleDoc,
|
|
21
|
+
DocumentationTree,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TacFileExtractor:
|
|
26
|
+
"""Extract documentation from a single .tac file."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, file_path: Path):
|
|
29
|
+
self.file_path = file_path
|
|
30
|
+
self.content = file_path.read_text()
|
|
31
|
+
self.lines = self.content.split("\n")
|
|
32
|
+
|
|
33
|
+
def extract_doc_blocks(self) -> List[DocBlock]:
|
|
34
|
+
"""
|
|
35
|
+
Extract --[[doc]] and --[[doc:parameter]] blocks.
|
|
36
|
+
|
|
37
|
+
Returns regular doc blocks (parameter blocks handled separately).
|
|
38
|
+
"""
|
|
39
|
+
doc_blocks = []
|
|
40
|
+
pattern = r"--\[\[doc\s*\n(.*?)\]\]"
|
|
41
|
+
|
|
42
|
+
for match in re.finditer(pattern, self.content, re.DOTALL):
|
|
43
|
+
content = match.group(1)
|
|
44
|
+
# Skip parameter docs (handled separately)
|
|
45
|
+
if content.strip().startswith(":parameter"):
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
# Find line number
|
|
49
|
+
line_num = self.content[: match.start()].count("\n") + 1
|
|
50
|
+
|
|
51
|
+
doc_blocks.append(DocBlock(content=content.strip(), line_number=line_num))
|
|
52
|
+
|
|
53
|
+
return doc_blocks
|
|
54
|
+
|
|
55
|
+
def extract_parameter_docs(self) -> List[ParameterDoc]:
|
|
56
|
+
"""
|
|
57
|
+
Extract --[[doc:parameter name]] blocks.
|
|
58
|
+
|
|
59
|
+
Format:
|
|
60
|
+
--[[doc:parameter <name>
|
|
61
|
+
Description here.
|
|
62
|
+
Type: string (optional)
|
|
63
|
+
Required: true/false (optional)
|
|
64
|
+
Default: value (optional)
|
|
65
|
+
]]
|
|
66
|
+
"""
|
|
67
|
+
parameter_docs = []
|
|
68
|
+
pattern = r"--\[\[doc:parameter\s+(\w+)\s*\n(.*?)\]\]"
|
|
69
|
+
|
|
70
|
+
for match in re.finditer(pattern, self.content, re.DOTALL):
|
|
71
|
+
param_name = match.group(1)
|
|
72
|
+
content = match.group(2).strip()
|
|
73
|
+
|
|
74
|
+
# Parse optional metadata
|
|
75
|
+
type_hint = None
|
|
76
|
+
required = True
|
|
77
|
+
default = None
|
|
78
|
+
|
|
79
|
+
# Extract type if present
|
|
80
|
+
type_match = re.search(r"Type:\s*(\S+)", content)
|
|
81
|
+
if type_match:
|
|
82
|
+
type_hint = type_match.group(1)
|
|
83
|
+
content = re.sub(r"Type:\s*\S+\s*\n?", "", content)
|
|
84
|
+
|
|
85
|
+
# Extract required if present
|
|
86
|
+
required_match = re.search(r"Required:\s*(true|false)", content, re.IGNORECASE)
|
|
87
|
+
if required_match:
|
|
88
|
+
required = required_match.group(1).lower() == "true"
|
|
89
|
+
content = re.sub(
|
|
90
|
+
r"Required:\s*(?:true|false)\s*\n?", "", content, flags=re.IGNORECASE
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Extract default if present
|
|
94
|
+
default_match = re.search(r"Default:\s*(.+)", content)
|
|
95
|
+
if default_match:
|
|
96
|
+
default = default_match.group(1).strip()
|
|
97
|
+
content = re.sub(r"Default:\s*.+\s*\n?", "", content)
|
|
98
|
+
|
|
99
|
+
parameter_docs.append(
|
|
100
|
+
ParameterDoc(
|
|
101
|
+
name=param_name,
|
|
102
|
+
description=content.strip(),
|
|
103
|
+
type_hint=type_hint,
|
|
104
|
+
required=required,
|
|
105
|
+
default=default,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return parameter_docs
|
|
110
|
+
|
|
111
|
+
def extract_bdd_features(self) -> List[BDDFeature]:
|
|
112
|
+
"""
|
|
113
|
+
Extract BDD features from Specification([[ ... ]]) blocks.
|
|
114
|
+
|
|
115
|
+
Parses Gherkin syntax:
|
|
116
|
+
Feature: Name
|
|
117
|
+
Scenario: Name
|
|
118
|
+
Given step
|
|
119
|
+
When step
|
|
120
|
+
Then step
|
|
121
|
+
"""
|
|
122
|
+
features = []
|
|
123
|
+
pattern = r"Specification\(\[\[(.*?)\]\]\)"
|
|
124
|
+
|
|
125
|
+
for match in re.finditer(pattern, self.content, re.DOTALL):
|
|
126
|
+
gherkin_content = match.group(1).strip()
|
|
127
|
+
line_num = self.content[: match.start()].count("\n") + 1
|
|
128
|
+
|
|
129
|
+
feature = self._parse_gherkin(gherkin_content, line_num)
|
|
130
|
+
if feature:
|
|
131
|
+
features.append(feature)
|
|
132
|
+
|
|
133
|
+
return features
|
|
134
|
+
|
|
135
|
+
def _parse_gherkin(self, gherkin: str, start_line: int) -> Optional[BDDFeature]:
|
|
136
|
+
"""Parse Gherkin syntax into BDDFeature."""
|
|
137
|
+
lines = gherkin.split("\n")
|
|
138
|
+
|
|
139
|
+
# Extract feature name and description
|
|
140
|
+
feature_name = None
|
|
141
|
+
feature_desc_lines = []
|
|
142
|
+
scenarios = []
|
|
143
|
+
current_scenario = None
|
|
144
|
+
current_steps = []
|
|
145
|
+
|
|
146
|
+
for line in lines:
|
|
147
|
+
line = line.strip()
|
|
148
|
+
|
|
149
|
+
if line.startswith("Feature:"):
|
|
150
|
+
feature_name = line[8:].strip()
|
|
151
|
+
|
|
152
|
+
elif line.startswith("Scenario:"):
|
|
153
|
+
# Save previous scenario if exists
|
|
154
|
+
if current_scenario:
|
|
155
|
+
scenarios.append(
|
|
156
|
+
BDDScenario(
|
|
157
|
+
name=current_scenario,
|
|
158
|
+
steps=current_steps,
|
|
159
|
+
line_number=start_line, # Approximate
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
current_scenario = line[9:].strip()
|
|
164
|
+
current_steps = []
|
|
165
|
+
|
|
166
|
+
elif any(line.startswith(kw) for kw in ["Given ", "When ", "Then ", "And ", "But "]):
|
|
167
|
+
# Extract keyword and text
|
|
168
|
+
for keyword in ["Given", "When", "Then", "And", "But"]:
|
|
169
|
+
if line.startswith(keyword + " "):
|
|
170
|
+
step_text = line[len(keyword) + 1 :].strip()
|
|
171
|
+
current_steps.append(BDDStep(keyword=keyword, text=step_text))
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
elif line and not feature_name:
|
|
175
|
+
# Description line before first scenario
|
|
176
|
+
feature_desc_lines.append(line)
|
|
177
|
+
|
|
178
|
+
# Save last scenario
|
|
179
|
+
if current_scenario:
|
|
180
|
+
scenarios.append(
|
|
181
|
+
BDDScenario(
|
|
182
|
+
name=current_scenario,
|
|
183
|
+
steps=current_steps,
|
|
184
|
+
line_number=start_line,
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if not feature_name:
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
feature_desc = "\n".join(feature_desc_lines).strip() if feature_desc_lines else None
|
|
192
|
+
|
|
193
|
+
return BDDFeature(
|
|
194
|
+
name=feature_name,
|
|
195
|
+
description=feature_desc,
|
|
196
|
+
scenarios=scenarios,
|
|
197
|
+
line_number=start_line,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def generate_code_examples(
|
|
201
|
+
self, doc_blocks: List[DocBlock], features: List[BDDFeature]
|
|
202
|
+
) -> List[CodeExample]:
|
|
203
|
+
"""
|
|
204
|
+
Generate code examples from doc blocks and BDD scenarios.
|
|
205
|
+
|
|
206
|
+
Extracts ```lua code blocks from doc blocks and generates
|
|
207
|
+
examples from BDD scenarios.
|
|
208
|
+
"""
|
|
209
|
+
examples = []
|
|
210
|
+
|
|
211
|
+
# Extract code blocks from doc blocks
|
|
212
|
+
for doc_block in doc_blocks:
|
|
213
|
+
code_blocks = re.findall(r"```lua\n(.*?)\n```", doc_block.content, re.DOTALL)
|
|
214
|
+
for idx, code in enumerate(code_blocks):
|
|
215
|
+
# Try to extract title from heading before code block
|
|
216
|
+
title = f"Example {idx + 1}"
|
|
217
|
+
heading_match = re.search(r"##\s+(.+?)\n```lua", doc_block.content, re.DOTALL)
|
|
218
|
+
if heading_match:
|
|
219
|
+
title = heading_match.group(1).strip()
|
|
220
|
+
|
|
221
|
+
examples.append(
|
|
222
|
+
CodeExample(
|
|
223
|
+
title=title,
|
|
224
|
+
code=code.strip(),
|
|
225
|
+
description=None,
|
|
226
|
+
source="doc_block",
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Generate examples from BDD scenarios
|
|
231
|
+
for feature in features:
|
|
232
|
+
for scenario in feature.scenarios:
|
|
233
|
+
code_lines = [f"-- Scenario: {scenario.name}"]
|
|
234
|
+
for step in scenario.steps:
|
|
235
|
+
code_lines.append(f"-- {step.keyword} {step.text}")
|
|
236
|
+
|
|
237
|
+
examples.append(
|
|
238
|
+
CodeExample(
|
|
239
|
+
title=scenario.name,
|
|
240
|
+
code="\n".join(code_lines),
|
|
241
|
+
description=f"From feature: {feature.name}",
|
|
242
|
+
source="bdd_scenario",
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return examples
|
|
247
|
+
|
|
248
|
+
def extract_module_doc(
|
|
249
|
+
self, module_name: str, full_module_name: str, module_dir: Optional[Path] = None
|
|
250
|
+
) -> ModuleDoc:
|
|
251
|
+
"""Extract complete documentation for this module."""
|
|
252
|
+
doc_blocks = self.extract_doc_blocks()
|
|
253
|
+
parameter_docs = self.extract_parameter_docs()
|
|
254
|
+
features = self.extract_bdd_features()
|
|
255
|
+
examples = self.generate_code_examples(doc_blocks, features)
|
|
256
|
+
|
|
257
|
+
# Use first doc block as overview
|
|
258
|
+
overview = doc_blocks[0].content if doc_blocks else None
|
|
259
|
+
|
|
260
|
+
# Look for index.md in module directory
|
|
261
|
+
index_content = None
|
|
262
|
+
if module_dir and module_dir.is_dir():
|
|
263
|
+
index_md = module_dir / "index.md"
|
|
264
|
+
if index_md.exists():
|
|
265
|
+
index_content = index_md.read_text()
|
|
266
|
+
|
|
267
|
+
return ModuleDoc(
|
|
268
|
+
name=module_name,
|
|
269
|
+
full_name=full_module_name,
|
|
270
|
+
file_path=str(self.file_path),
|
|
271
|
+
doc_blocks=doc_blocks,
|
|
272
|
+
overview=overview,
|
|
273
|
+
index_content=index_content,
|
|
274
|
+
parameters=parameter_docs,
|
|
275
|
+
features=features,
|
|
276
|
+
examples=examples,
|
|
277
|
+
line_count=len(self.lines),
|
|
278
|
+
has_specs=len(features) > 0,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class DirectoryExtractor:
|
|
283
|
+
"""Extract documentation from all .tac files in a directory."""
|
|
284
|
+
|
|
285
|
+
def __init__(self, root_path: Path):
|
|
286
|
+
self.root_path = root_path
|
|
287
|
+
|
|
288
|
+
def extract_all(self) -> DocumentationTree:
|
|
289
|
+
"""Extract documentation from all .tac files."""
|
|
290
|
+
modules = []
|
|
291
|
+
|
|
292
|
+
# Find all .spec.tac files
|
|
293
|
+
for spec_file in self.root_path.rglob("*.spec.tac"):
|
|
294
|
+
# Derive module name from file path
|
|
295
|
+
# e.g., tactus/stdlib/tac/tactus/classify.spec.tac -> tactus.classify
|
|
296
|
+
relative = spec_file.relative_to(self.root_path)
|
|
297
|
+
parts = relative.parts
|
|
298
|
+
|
|
299
|
+
# Remove .spec.tac extension
|
|
300
|
+
module_name = spec_file.stem.replace(".spec", "")
|
|
301
|
+
|
|
302
|
+
# Build full module name from path
|
|
303
|
+
# If path is tactus/stdlib/tac/tactus/classify.spec.tac
|
|
304
|
+
# -> tactus.classify
|
|
305
|
+
if "tactus" in parts:
|
|
306
|
+
# Find first "tactus" and build from there
|
|
307
|
+
tactus_idx = parts.index("tactus")
|
|
308
|
+
module_parts = list(parts[tactus_idx:-1]) + [module_name]
|
|
309
|
+
full_module_name = ".".join(module_parts)
|
|
310
|
+
else:
|
|
311
|
+
full_module_name = module_name
|
|
312
|
+
|
|
313
|
+
# Find module directory (parent of spec file or sibling directory)
|
|
314
|
+
# For tactus/stdlib/tac/tactus/classify.spec.tac
|
|
315
|
+
# -> look for tactus/stdlib/tac/tactus/classify/ directory
|
|
316
|
+
module_dir = spec_file.parent / module_name
|
|
317
|
+
if not module_dir.is_dir():
|
|
318
|
+
# Fallback: spec file's parent is the module directory
|
|
319
|
+
module_dir = spec_file.parent
|
|
320
|
+
|
|
321
|
+
# Extract documentation
|
|
322
|
+
extractor = TacFileExtractor(spec_file)
|
|
323
|
+
module_doc = extractor.extract_module_doc(module_name, full_module_name, module_dir)
|
|
324
|
+
modules.append(module_doc)
|
|
325
|
+
|
|
326
|
+
return DocumentationTree(root_path=str(self.root_path), modules=modules)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Render Tactus documentation as HTML.
|
|
3
|
+
|
|
4
|
+
Uses Jinja2 templates with clean, minimal styling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
10
|
+
import markdown
|
|
11
|
+
from tactus.docs.models import DocumentationTree, ModuleDoc
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HTMLRenderer:
|
|
15
|
+
"""Render documentation tree as HTML files."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, template_dir: Optional[Path] = None):
|
|
18
|
+
"""
|
|
19
|
+
Initialize renderer with templates.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
template_dir: Path to Jinja2 templates. If None, uses built-in templates.
|
|
23
|
+
"""
|
|
24
|
+
if template_dir is None:
|
|
25
|
+
template_dir = Path(__file__).parent / "templates"
|
|
26
|
+
|
|
27
|
+
self.env = Environment(
|
|
28
|
+
loader=FileSystemLoader(str(template_dir)),
|
|
29
|
+
autoescape=select_autoescape(["html", "xml"]),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Add custom filters
|
|
33
|
+
self.env.filters["markdown"] = self._markdown_filter
|
|
34
|
+
|
|
35
|
+
def _markdown_filter(self, text: str) -> str:
|
|
36
|
+
"""Convert markdown to HTML."""
|
|
37
|
+
return markdown.markdown(
|
|
38
|
+
text,
|
|
39
|
+
extensions=["extra", "codehilite", "fenced_code"],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def render_module(self, module: ModuleDoc) -> str:
|
|
43
|
+
"""Render a single module's documentation."""
|
|
44
|
+
template = self.env.get_template("module.html")
|
|
45
|
+
return template.render(module=module)
|
|
46
|
+
|
|
47
|
+
def render_index(self, tree: DocumentationTree) -> str:
|
|
48
|
+
"""Render the index page listing all modules."""
|
|
49
|
+
template = self.env.get_template("index.html")
|
|
50
|
+
return template.render(tree=tree)
|
|
51
|
+
|
|
52
|
+
def render_all(self, tree: DocumentationTree, output_dir: Path):
|
|
53
|
+
"""
|
|
54
|
+
Render all documentation to HTML files.
|
|
55
|
+
|
|
56
|
+
Creates:
|
|
57
|
+
- index.html (list of all modules)
|
|
58
|
+
- <module-name>.html for each module
|
|
59
|
+
"""
|
|
60
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
|
|
62
|
+
# Render index
|
|
63
|
+
index_html = self.render_index(tree)
|
|
64
|
+
(output_dir / "index.html").write_text(index_html)
|
|
65
|
+
|
|
66
|
+
# Render each module
|
|
67
|
+
for module in tree.modules:
|
|
68
|
+
module_html = self.render_module(module)
|
|
69
|
+
filename = f"{module.name}.html"
|
|
70
|
+
(output_dir / filename).write_text(module_html)
|
|
71
|
+
|
|
72
|
+
print(f"✓ Generated {len(tree.modules) + 1} HTML files in {output_dir}")
|
tactus/docs/models.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic models for Tactus documentation structure.
|
|
3
|
+
|
|
4
|
+
These models represent the extracted documentation from .tac files,
|
|
5
|
+
including doc blocks, BDD specifications, and code examples.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DocBlock(BaseModel):
|
|
13
|
+
"""
|
|
14
|
+
A --[[doc]] comment block extracted from a .tac file.
|
|
15
|
+
|
|
16
|
+
Contains markdown-formatted documentation text.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
content: str = Field(..., description="Markdown content of the doc block")
|
|
20
|
+
line_number: int = Field(..., description="Starting line number in source file")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ParameterDoc(BaseModel):
|
|
24
|
+
"""
|
|
25
|
+
Documentation for a single parameter extracted from --[[doc:parameter name]] blocks.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
name: str = Field(..., description="Parameter name")
|
|
29
|
+
description: str = Field(..., description="Parameter description")
|
|
30
|
+
type_hint: Optional[str] = Field(None, description="Type hint if specified")
|
|
31
|
+
required: bool = Field(True, description="Whether parameter is required")
|
|
32
|
+
default: Optional[str] = Field(None, description="Default value if specified")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BDDStep(BaseModel):
|
|
36
|
+
"""
|
|
37
|
+
A single Given/When/Then step in a BDD scenario.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
keyword: str = Field(..., description="Given, When, Then, And, But")
|
|
41
|
+
text: str = Field(..., description="Step text")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BDDScenario(BaseModel):
|
|
45
|
+
"""
|
|
46
|
+
A BDD scenario from a Specification block.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
name: str = Field(..., description="Scenario name")
|
|
50
|
+
steps: List[BDDStep] = Field(default_factory=list, description="Scenario steps")
|
|
51
|
+
line_number: int = Field(..., description="Starting line number")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class BDDFeature(BaseModel):
|
|
55
|
+
"""
|
|
56
|
+
A BDD feature containing multiple scenarios.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
name: str = Field(..., description="Feature name")
|
|
60
|
+
description: Optional[str] = Field(None, description="Feature description")
|
|
61
|
+
scenarios: List[BDDScenario] = Field(default_factory=list, description="Scenarios")
|
|
62
|
+
line_number: int = Field(..., description="Starting line number")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class CodeExample(BaseModel):
|
|
66
|
+
"""
|
|
67
|
+
A code example extracted from doc blocks or generated from BDD scenarios.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
title: str = Field(..., description="Example title")
|
|
71
|
+
code: str = Field(..., description="Lua code")
|
|
72
|
+
description: Optional[str] = Field(None, description="Example description")
|
|
73
|
+
source: str = Field(..., description="Source: 'doc_block' or 'bdd_scenario'")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ModuleDoc(BaseModel):
|
|
77
|
+
"""
|
|
78
|
+
Complete documentation for a Tactus module (e.g., tactus.classify).
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
name: str = Field(..., description="Module name (e.g., 'classify')")
|
|
82
|
+
full_name: str = Field(..., description="Full module path (e.g., 'tactus.classify')")
|
|
83
|
+
file_path: str = Field(..., description="Path to source .tac file")
|
|
84
|
+
|
|
85
|
+
# Main documentation
|
|
86
|
+
doc_blocks: List[DocBlock] = Field(default_factory=list, description="All doc blocks")
|
|
87
|
+
overview: Optional[str] = Field(None, description="Module overview from first doc block")
|
|
88
|
+
index_content: Optional[str] = Field(None, description="Content from index.md if present")
|
|
89
|
+
|
|
90
|
+
# Parameters/Configuration
|
|
91
|
+
parameters: List[ParameterDoc] = Field(default_factory=list, description="Parameter docs")
|
|
92
|
+
|
|
93
|
+
# BDD Specifications
|
|
94
|
+
features: List[BDDFeature] = Field(default_factory=list, description="BDD features")
|
|
95
|
+
|
|
96
|
+
# Code Examples
|
|
97
|
+
examples: List[CodeExample] = Field(default_factory=list, description="Code examples")
|
|
98
|
+
|
|
99
|
+
# Metadata
|
|
100
|
+
line_count: int = Field(0, description="Total lines in source file")
|
|
101
|
+
has_specs: bool = Field(False, description="Whether module has BDD specs")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class DocumentationTree(BaseModel):
|
|
105
|
+
"""
|
|
106
|
+
Tree structure representing all documentation in a directory.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
root_path: str = Field(..., description="Root directory path")
|
|
110
|
+
modules: List[ModuleDoc] = Field(default_factory=list, description="All documented modules")
|
|
111
|
+
|
|
112
|
+
def get_module(self, name: str) -> Optional[ModuleDoc]:
|
|
113
|
+
"""Get module by name."""
|
|
114
|
+
for module in self.modules:
|
|
115
|
+
if module.name == name or module.full_name == name:
|
|
116
|
+
return module
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
def get_modules_by_prefix(self, prefix: str) -> List[ModuleDoc]:
|
|
120
|
+
"""Get all modules matching a prefix (e.g., 'tactus.classify')."""
|
|
121
|
+
return [m for m in self.modules if m.full_name.startswith(prefix)]
|