codetrail-cli 1.4.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.
Files changed (171) hide show
  1. codetrail/__init__.py +3 -0
  2. codetrail/__main__.py +5 -0
  3. codetrail/agents/__init__.py +4 -0
  4. codetrail/agents/convert.py +92 -0
  5. codetrail/agents/export.py +284 -0
  6. codetrail/agents/instructions.py +42 -0
  7. codetrail/agents/protocol.py +111 -0
  8. codetrail/agents/tools.py +185 -0
  9. codetrail/assistant/__init__.py +161 -0
  10. codetrail/assistant/claude_code.py +330 -0
  11. codetrail/assistant/codex.py +260 -0
  12. codetrail/assistant/environment.py +66 -0
  13. codetrail/assistant/estimate.py +225 -0
  14. codetrail/assistant/fake.py +74 -0
  15. codetrail/assistant/guard.py +80 -0
  16. codetrail/assistant/guard_hook.py +58 -0
  17. codetrail/assistant/local.py +245 -0
  18. codetrail/assistant/local_tools.py +177 -0
  19. codetrail/assistant/prompts.py +404 -0
  20. codetrail/assistant/routing.py +86 -0
  21. codetrail/assistant/runner.py +120 -0
  22. codetrail/assistant/status.py +186 -0
  23. codetrail/assistant/usage.py +93 -0
  24. codetrail/bridge.py +292 -0
  25. codetrail/cli.py +407 -0
  26. codetrail/config.py +485 -0
  27. codetrail/database/__init__.py +60 -0
  28. codetrail/database/migrations/0001_facts.sql +34 -0
  29. codetrail/database/migrations/0002_settings.sql +5 -0
  30. codetrail/database/migrations/0003_learning.sql +34 -0
  31. codetrail/database/migrations/0004_assistant_usage.sql +24 -0
  32. codetrail/database/migrations/0005_page_failures.sql +7 -0
  33. codetrail/database/migrations/0006_metric_values.sql +9 -0
  34. codetrail/errors.py +5 -0
  35. codetrail/extract/__init__.py +249 -0
  36. codetrail/extract/adr.py +98 -0
  37. codetrail/extract/github_actions.py +140 -0
  38. codetrail/extract/openapi.py +101 -0
  39. codetrail/extract/python.py +221 -0
  40. codetrail/extract/swift.py +204 -0
  41. codetrail/extract/terraform.py +166 -0
  42. codetrail/extract/typescript.py +464 -0
  43. codetrail/facts/__init__.py +112 -0
  44. codetrail/facts/store.py +209 -0
  45. codetrail/generate/__init__.py +1 -0
  46. codetrail/generate/failures.py +31 -0
  47. codetrail/generate/outline.py +159 -0
  48. codetrail/generate/revise.py +72 -0
  49. codetrail/generate/run.py +530 -0
  50. codetrail/generate/scope.py +135 -0
  51. codetrail/generate/validate.py +218 -0
  52. codetrail/guide/__init__.py +147 -0
  53. codetrail/learn/__init__.py +143 -0
  54. codetrail/learn/routes.py +139 -0
  55. codetrail/locales/ar/LC_MESSAGES/codetrail.mo +0 -0
  56. codetrail/locales/ar/LC_MESSAGES/codetrail.po +2236 -0
  57. codetrail/locales/codetrail.pot +1942 -0
  58. codetrail/lock.py +114 -0
  59. codetrail/metrics/__init__.py +6 -0
  60. codetrail/metrics/activity.py +96 -0
  61. codetrail/metrics/churn.py +156 -0
  62. codetrail/metrics/commits.py +45 -0
  63. codetrail/metrics/coverage.py +114 -0
  64. codetrail/metrics/decisions.py +77 -0
  65. codetrail/metrics/history.py +47 -0
  66. codetrail/metrics/inventory.py +63 -0
  67. codetrail/metrics/rationale.py +74 -0
  68. codetrail/metrics/releases.py +26 -0
  69. codetrail/metrics/report.py +111 -0
  70. codetrail/metrics/repository.py +128 -0
  71. codetrail/metrics/size.py +95 -0
  72. codetrail/py.typed +0 -0
  73. codetrail/remove.py +50 -0
  74. codetrail/repo/__init__.py +4 -0
  75. codetrail/repo/git.py +56 -0
  76. codetrail/repo/gitleaks.toml +6 -0
  77. codetrail/repo/history.py +262 -0
  78. codetrail/repo/mirror.py +137 -0
  79. codetrail/repo/refresh.py +41 -0
  80. codetrail/repo/rules.py +83 -0
  81. codetrail/repo/secrets.py +145 -0
  82. codetrail/repo/signal.py +65 -0
  83. codetrail/repo/source.py +166 -0
  84. codetrail/search.py +178 -0
  85. codetrail/server.py +45 -0
  86. codetrail/system.py +484 -0
  87. codetrail/update.py +276 -0
  88. codetrail/web/__init__.py +1 -0
  89. codetrail/web/app.py +897 -0
  90. codetrail/web/diagrams.py +414 -0
  91. codetrail/web/documentation.py +146 -0
  92. codetrail/web/i18n.py +44 -0
  93. codetrail/web/navigation.py +127 -0
  94. codetrail/web/render.py +126 -0
  95. codetrail/web/repository_view.py +230 -0
  96. codetrail/web/security.py +171 -0
  97. codetrail/web/static/brand/favicon.svg +1 -0
  98. codetrail/web/static/brand/mark.svg +1 -0
  99. codetrail/web/static/css/base.css +192 -0
  100. codetrail/web/static/css/components.css +2254 -0
  101. codetrail/web/static/css/content.css +310 -0
  102. codetrail/web/static/css/layout.css +410 -0
  103. codetrail/web/static/css/print.css +42 -0
  104. codetrail/web/static/css/tokens.css +228 -0
  105. codetrail/web/static/js/api.js +49 -0
  106. codetrail/web/static/js/ask.js +182 -0
  107. codetrail/web/static/js/d2-worker.js +34 -0
  108. codetrail/web/static/js/d2.js +28 -0
  109. codetrail/web/static/js/diagram.js +52 -0
  110. codetrail/web/static/js/documentation.js +24 -0
  111. codetrail/web/static/js/learning.js +47 -0
  112. codetrail/web/static/js/main.js +38 -0
  113. codetrail/web/static/js/outline.js +44 -0
  114. codetrail/web/static/js/palette.js +211 -0
  115. codetrail/web/static/js/repository.js +528 -0
  116. codetrail/web/static/js/resize.js +126 -0
  117. codetrail/web/static/js/shortcuts.js +87 -0
  118. codetrail/web/static/js/sidebar.js +194 -0
  119. codetrail/web/static/js/theme-init.js +27 -0
  120. codetrail/web/static/js/theme.js +42 -0
  121. codetrail/web/static/js/update.js +107 -0
  122. codetrail/web/static/vendor/d2/LICENSE +375 -0
  123. codetrail/web/static/vendor/d2/LICENSE-elk +263 -0
  124. codetrail/web/static/vendor/d2/LICENSE-wasm_exec +27 -0
  125. codetrail/web/static/vendor/d2/VERSION +12 -0
  126. codetrail/web/static/vendor/d2/d2.wasm +0 -0
  127. codetrail/web/static/vendor/d2/elk.js +105914 -0
  128. codetrail/web/static/vendor/d2/wasm_exec.js +656 -0
  129. codetrail/web/static/vendor/inter/InterVariable-Italic.woff2 +0 -0
  130. codetrail/web/static/vendor/inter/InterVariable.woff2 +0 -0
  131. codetrail/web/static/vendor/inter/LICENSE.txt +92 -0
  132. codetrail/web/static/vendor/inter/VERSION +6 -0
  133. codetrail/web/static/vendor/noto-sans-arabic/NotoSansArabic-wght.woff2 +0 -0
  134. codetrail/web/static/vendor/noto-sans-arabic/OFL.txt +93 -0
  135. codetrail/web/static/vendor/noto-sans-arabic/VERSION +12 -0
  136. codetrail/web/target_view.py +63 -0
  137. codetrail/web/templates/answers.html +25 -0
  138. codetrail/web/templates/area.html +46 -0
  139. codetrail/web/templates/base.html +37 -0
  140. codetrail/web/templates/decisions.html +30 -0
  141. codetrail/web/templates/diagram.html +10 -0
  142. codetrail/web/templates/digests.html +24 -0
  143. codetrail/web/templates/documentation.html +247 -0
  144. codetrail/web/templates/error.html +9 -0
  145. codetrail/web/templates/estimate.html +48 -0
  146. codetrail/web/templates/fact.html +29 -0
  147. codetrail/web/templates/home.html +196 -0
  148. codetrail/web/templates/layout/ask_panel.html +27 -0
  149. codetrail/web/templates/layout/header.html +32 -0
  150. codetrail/web/templates/layout/icons.html +27 -0
  151. codetrail/web/templates/layout/outline.html +4 -0
  152. codetrail/web/templates/layout/palette.html +27 -0
  153. codetrail/web/templates/layout/shortcuts.html +27 -0
  154. codetrail/web/templates/layout/sidebar.html +79 -0
  155. codetrail/web/templates/layout/update_dialog.html +18 -0
  156. codetrail/web/templates/macros.html +13 -0
  157. codetrail/web/templates/page.html +93 -0
  158. codetrail/web/templates/progress.html +46 -0
  159. codetrail/web/templates/repository.html +369 -0
  160. codetrail/web/templates/search.html +31 -0
  161. codetrail/web/templates/source.html +15 -0
  162. codetrail/web/templates/system.html +43 -0
  163. codetrail/web/templates/system_arrows.html +17 -0
  164. codetrail/web/templates/system_legend.html +9 -0
  165. codetrail/web/templates/update_log.html +30 -0
  166. codetrail/web/trend.py +22 -0
  167. codetrail_cli-1.4.1.dist-info/METADATA +372 -0
  168. codetrail_cli-1.4.1.dist-info/RECORD +171 -0
  169. codetrail_cli-1.4.1.dist-info/WHEEL +4 -0
  170. codetrail_cli-1.4.1.dist-info/entry_points.txt +3 -0
  171. codetrail_cli-1.4.1.dist-info/licenses/LICENSE +202 -0
codetrail/update.py ADDED
@@ -0,0 +1,276 @@
1
+ """An update: refresh the sources, extract and record the facts, and write the guide, under the target's lock.
2
+
3
+ Facts are recorded before the guide is written: they are true whatever happens to the guide, and a page that isn't
4
+ rewritten stays affected, because that is derived from its front matter (design section 6.8).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import threading
11
+ from collections.abc import Callable, Mapping
12
+ from dataclasses import dataclass, replace
13
+ from datetime import date
14
+ from pathlib import Path
15
+
16
+ import anyio
17
+
18
+ from codetrail.agents.export import build_export, write_export
19
+ from codetrail.assistant import Assistant
20
+ from codetrail.assistant.estimate import PageToWrite, UpdateEstimate, estimate_update
21
+ from codetrail.assistant.routing import build_assistant
22
+ from codetrail.assistant.status import require_ready
23
+ from codetrail.assistant.usage import UsageLog
24
+ from codetrail.config import (
25
+ ExtractSettings,
26
+ GlobalConfig,
27
+ Paths,
28
+ TargetConfig,
29
+ check_containment,
30
+ load_global,
31
+ load_target,
32
+ model_choice,
33
+ )
34
+ from codetrail.database import connect
35
+ from codetrail.errors import CodetrailError
36
+ from codetrail.extract import Extraction, Extractor, check_facts, run_extractors
37
+ from codetrail.extract.adr import AdrExtractor
38
+ from codetrail.extract.github_actions import GitHubActionsExtractor
39
+ from codetrail.extract.openapi import OpenApiExtractor
40
+ from codetrail.extract.python import PythonExtractor
41
+ from codetrail.extract.swift import SwiftExtractor
42
+ from codetrail.extract.terraform import TerraformExtractor
43
+ from codetrail.extract.typescript import TypeScriptExtractor
44
+ from codetrail.facts import FactDiff, Snapshot
45
+ from codetrail.facts.store import FactStore
46
+ from codetrail.generate.failures import FailedPages
47
+ from codetrail.generate.run import (
48
+ PAGE_ATTEMPTS,
49
+ GenerationContext,
50
+ GenerationResult,
51
+ PlannedWork,
52
+ Progress,
53
+ generate_guide,
54
+ no_progress,
55
+ planned_work,
56
+ )
57
+ from codetrail.guide import GuideRepository
58
+ from codetrail.learn import LearningState
59
+ from codetrail.lock import target_in_use, target_lock
60
+ from codetrail.metrics.history import record_values
61
+ from codetrail.metrics.report import Metric, Value, build_report
62
+ from codetrail.metrics.repository import build_repository_report
63
+ from codetrail.repo.mirror import read_file_at
64
+ from codetrail.repo.refresh import TARGET_IGNORE_FILE, ignore_lines, refresh_while_locked
65
+ from codetrail.repo.rules import ExclusionRules
66
+ from codetrail.repo.secrets import SecretScanner
67
+ from codetrail.repo.source import SourceManifest, allowed_reader
68
+ from codetrail.system import derive
69
+
70
+ STOP_CHECK_SECONDS = 0.2 # how often an update started from the page checks whether the server is stopping
71
+ STOPPED = "The server stopped, so the update stopped; the guide wasn't changed."
72
+ logger = logging.getLogger(__name__)
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class UpdateResult:
77
+ manifest: SourceManifest
78
+ snapshot: Snapshot
79
+ diff: FactDiff
80
+ extraction: Extraction
81
+ generation: GenerationResult | None = None
82
+ estimate: UpdateEstimate | None = None
83
+ declined: bool = False # the reader didn't confirm the estimate; facts were refreshed, the guide wasn't touched
84
+
85
+
86
+ def _calls(target: TargetConfig, work: PlannedWork) -> list[tuple[str, str, str, int, int]]:
87
+ """The update's paid calls for its estimate: kind, provider, model, expected count, maximum count."""
88
+ plan, write, digest = (model_choice(getattr(target.models, kind)) for kind in ("plan", "write", "digest"))
89
+ return [
90
+ ("plan", *plan, work.plan_calls, work.plan_calls),
91
+ ("write", *write, work.pages_expected, work.page_calls_max),
92
+ ("revise", *write, work.revisions_expected, work.revisions_expected * PAGE_ATTEMPTS), # the write model
93
+ ("digest", *digest, int(work.digest_expected), int(work.digest_possible)),
94
+ ]
95
+
96
+
97
+ def with_system(extraction: Extraction, source: Path, files: Mapping[str, str], extract: ExtractSettings) -> Extraction:
98
+ """The extraction with the system pass's parts and connections, checked like extracted facts (design 17.3)."""
99
+ found = derive(
100
+ extraction.entities, extraction.relations, files, allowed_reader(source, files, extract.max_file_bytes)
101
+ )
102
+ entities, relations, warnings = check_facts(found.entities, found.relations, extract.max_attribute_chars)
103
+ return replace(
104
+ extraction,
105
+ entities=sorted([*extraction.entities, *entities], key=lambda entity: entity.id),
106
+ relations=sorted([*extraction.relations, *relations], key=lambda relation: relation.key),
107
+ warnings=[*extraction.warnings, *found.warnings, *warnings],
108
+ system=dict(found.counts),
109
+ )
110
+
111
+
112
+ def build_extractors(target: TargetConfig, extract: ExtractSettings | None = None) -> list[Extractor]:
113
+ extract = extract or ExtractSettings()
114
+ available: dict[str, Extractor] = {
115
+ "python": PythonExtractor(),
116
+ "adr": AdrExtractor(target.adr.paths),
117
+ "openapi": OpenApiExtractor(target.openapi.paths),
118
+ "terraform": TerraformExtractor(extract.max_resource_paths),
119
+ "swift": SwiftExtractor(),
120
+ "typescript": TypeScriptExtractor(extract.max_tsconfig_paths),
121
+ "github_actions": GitHubActionsExtractor(extract.max_workflow_steps),
122
+ }
123
+ return [available[name] for name in target.extractors]
124
+
125
+
126
+ def record_metrics(paths: Paths, name: str, settings: GlobalConfig, store: FactStore) -> None:
127
+ """Writes each documentation metric's and repository statistic's value at the latest snapshot (sections 18.3, 19.4).
128
+
129
+ The trend is secondary to the guide, so a failure here is logged and never fails the update; each report is built
130
+ on its own, so one failing still writes the other's values.
131
+ """
132
+ snapshot = store.latest_snapshot()
133
+ if snapshot is None:
134
+ return
135
+ today = date.today()
136
+ values: dict[Metric, Value] = {}
137
+ try:
138
+ values |= build_report(paths, name, settings, store, today).values()
139
+ except Exception as error:
140
+ logger.warning("The documentation metrics weren't recorded: %s", error)
141
+ try:
142
+ values |= build_repository_report(paths, name, settings, store, today).values()
143
+ except Exception as error:
144
+ logger.warning("The repository statistics weren't recorded: %s", error)
145
+ if values:
146
+ try:
147
+ record_values(store.connection, snapshot.id, values)
148
+ except Exception as error:
149
+ logger.warning("The metrics' values weren't recorded: %s", error)
150
+
151
+
152
+ async def generate_until_stopped(
153
+ context: GenerationContext, claude: Assistant, stop: threading.Event
154
+ ) -> GenerationResult:
155
+ """Writes the guide, cancelled once `stop` is set, so its cleanup runs: each program's process group is killed and
156
+ the guide's uncommitted changes are discarded (design section 15.5)."""
157
+ if stop.is_set(): # a confirmation that raced the stop: no program starts
158
+ raise CodetrailError(STOPPED)
159
+ generation: GenerationResult | None = None
160
+ try:
161
+ async with anyio.create_task_group() as group:
162
+
163
+ async def cancel_when_stopped() -> None:
164
+ while not stop.is_set():
165
+ await anyio.sleep(STOP_CHECK_SECONDS)
166
+ group.cancel_scope.cancel()
167
+
168
+ group.start_soon(cancel_when_stopped)
169
+ generation = await generate_guide(context, claude)
170
+ group.cancel_scope.cancel()
171
+ except BaseExceptionGroup as errors: # only the generation raises, and the group wraps its error
172
+ raise errors.exceptions[0] from None
173
+ if generation is None:
174
+ raise CodetrailError(STOPPED)
175
+ return generation
176
+
177
+
178
+ def run_update(
179
+ paths: Paths,
180
+ name: str,
181
+ claude: Assistant | None = None,
182
+ facts_only: bool = False,
183
+ confirm: Callable[[UpdateEstimate], bool] | None = None,
184
+ progress: Progress = no_progress,
185
+ stop: threading.Event | None = None,
186
+ retry_failed: bool = False,
187
+ ) -> UpdateResult:
188
+ """Refreshes the sources and facts (free), then, unless `facts_only`, estimates the guide's paid work and asks
189
+ `confirm` before doing it (design section 15.4). An update with a real assistant always needs `confirm`.
190
+ Each step is reported to `progress` as it happens. Setting `stop` (the server stopping) cancels the guide's
191
+ writing. Pages that failed validation wait until their facts change, unless `retry_failed`."""
192
+ with target_in_use(paths, name): # so the target isn't removed during the update
193
+ target = load_target(paths, name)
194
+ check_containment(paths, target.repository) # before the lock creates the data folder
195
+ settings = load_global(paths)
196
+ sign_ins: dict[str, str] = {}
197
+ if claude is None and not facts_only: # before any work: the providers this update uses must be ready
198
+ if confirm is None:
199
+ raise CodetrailError("An update that calls an assistant must show its estimate and be confirmed first.")
200
+ statuses = require_ready(target, settings, kinds=("plan", "write", "digest"))
201
+ sign_ins = {status.provider: status.method for status in statuses}
202
+ data = paths.target_data(name)
203
+ with target_lock(paths, name):
204
+ progress({"step": "facts"})
205
+ manifest = refresh_while_locked(paths, name)
206
+ source = data / "source"
207
+ extraction = run_extractors(
208
+ source, manifest.files, build_extractors(target, settings.extract), settings.extract.max_file_bytes,
209
+ settings.extract.max_attribute_chars,
210
+ ) # fmt: skip
211
+ extraction = with_system(extraction, source, manifest.files, settings.extract)
212
+ connection = connect(data / "codetrail.db")
213
+ try:
214
+ store = FactStore(connection)
215
+
216
+ def export() -> None: # the context for coding agents, rebuilt from what this update recorded
217
+ write_export(data / "agents", build_export(name, target, GuideRepository(data / "guide"), store,
218
+ manifest, settings)) # fmt: skip
219
+
220
+ snapshot, diff = store.record(manifest.commit, extraction.entities, extraction.relations)
221
+ progress({"step": "facts_recorded", "changes": len(diff.changed_ids())})
222
+ if facts_only:
223
+ record_metrics(paths, name, settings, store)
224
+ export()
225
+ return UpdateResult(manifest, snapshot, diff, extraction)
226
+ previous = store.previous_snapshot(snapshot)
227
+ mirror = data / "mirror.git"
228
+ rules = ExclusionRules(
229
+ ignore_lines(paths, name, read_file_at(mirror, manifest.commit, TARGET_IGNORE_FILE))
230
+ )
231
+ excluded = manifest.excluded_paths()
232
+ context = GenerationContext(
233
+ target=name,
234
+ guide=GuideRepository(data / "guide"),
235
+ store=store,
236
+ manifest=manifest,
237
+ source_root=source,
238
+ mirror=mirror,
239
+ scanner=SecretScanner(settings.tools),
240
+ visible=lambda path: rules.reason(path) is None and path not in excluded,
241
+ max_pages=target.generation.max_pages_per_update,
242
+ concurrency=target.generation.concurrency,
243
+ max_budget_usd=target.generation.max_budget_usd_per_update,
244
+ max_tokens=target.generation.max_tokens_per_update,
245
+ previous_commit=previous.commit if previous else None,
246
+ diff=diff,
247
+ learned=LearningState(connection).learned_page_ids(),
248
+ usage=UsageLog(connection, settings.prices),
249
+ progress=progress,
250
+ failed_pages=FailedPages(connection),
251
+ retry_failed=retry_failed,
252
+ revise_max_changes=target.generation.revise_max_changes,
253
+ max_brief_words=settings.agents.max_brief_words,
254
+ )
255
+ if confirm is not None:
256
+ work = planned_work(context)
257
+ estimate = estimate_update(connection, _calls(target, work), settings.estimates, settings.prices,
258
+ sign_ins, target.generation.max_budget_usd_per_update,
259
+ target.generation.max_tokens_per_update,
260
+ [PageToWrite(page.entry.title, page.reason, page.changed_now,
261
+ bool(page.changes)) for page in work.pages],
262
+ [entry.title for entry in work.skipped]) # fmt: skip
263
+ if not confirm(estimate):
264
+ record_metrics(paths, name, settings, store)
265
+ export()
266
+ return UpdateResult(manifest, snapshot, diff, extraction, None, estimate, declined=True)
267
+ writer = claude or build_assistant(source, settings, target)
268
+ if stop is None:
269
+ generation = anyio.run(generate_guide, context, writer)
270
+ else:
271
+ generation = anyio.run(generate_until_stopped, context, writer, stop)
272
+ record_metrics(paths, name, settings, store)
273
+ export()
274
+ finally:
275
+ connection.close()
276
+ return UpdateResult(manifest, snapshot, diff, extraction, generation)
@@ -0,0 +1 @@
1
+ """The local page: FastAPI routes, templates, diagrams and the security model (design section 7)."""