maxitor 3.11.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 (185) hide show
  1. maxitor/graph_builder/__init__.py +20 -0
  2. maxitor/graph_builder/cli.py +136 -0
  3. maxitor/graph_builder/config.py +141 -0
  4. maxitor/graph_builder/config_schema.json +105 -0
  5. maxitor/graph_builder/differences.py +134 -0
  6. maxitor/graph_builder/py.typed +0 -0
  7. maxitor/graph_builder/schemas/1.0.json +1476 -0
  8. maxitor/graph_builder/schemas/1.1.json +1481 -0
  9. maxitor/graph_builder/schemas/1.2.json +1518 -0
  10. maxitor/graph_builder/schemas/1.3.json +1600 -0
  11. maxitor/graph_builder/schemas/__init__.py +154 -0
  12. maxitor/graph_builder/writer.py +182 -0
  13. maxitor/graph_compare/__init__.py +50 -0
  14. maxitor/graph_compare/changed_inside.py +141 -0
  15. maxitor/graph_compare/compare.py +150 -0
  16. maxitor/graph_compare/comparison.py +143 -0
  17. maxitor/graph_compare/edge_ids.py +78 -0
  18. maxitor/graph_compare/py.typed +0 -0
  19. maxitor/graph_map/__init__.py +50 -0
  20. maxitor/graph_map/areas.py +272 -0
  21. maxitor/graph_map/assemble.py +314 -0
  22. maxitor/graph_map/layout.py +67 -0
  23. maxitor/graph_map/places.py +117 -0
  24. maxitor/graph_map/py.typed +0 -0
  25. maxitor/graph_map/union.py +88 -0
  26. maxitor/repo_graph_port/__init__.py +23 -0
  27. maxitor/repo_graph_port/actions/__init__.py +17 -0
  28. maxitor/repo_graph_port/actions/describe_history_action.py +113 -0
  29. maxitor/repo_graph_port/actions/history_action.py +157 -0
  30. maxitor/repo_graph_port/actions/receive_host_event_action.py +119 -0
  31. maxitor/repo_graph_port/actions/report_security_event_action.py +89 -0
  32. maxitor/repo_graph_port/actions/version_action.py +106 -0
  33. maxitor/repo_graph_port/auth/__init__.py +19 -0
  34. maxitor/repo_graph_port/auth/history_key_auth_coordinator.py +104 -0
  35. maxitor/repo_graph_port/auth/history_key_authenticator.py +58 -0
  36. maxitor/repo_graph_port/auth/history_key_credential_extractor.py +38 -0
  37. maxitor/repo_graph_port/build_graph.py +35 -0
  38. maxitor/repo_graph_port/cli.py +207 -0
  39. maxitor/repo_graph_port/commit.py +35 -0
  40. maxitor/repo_graph_port/contribution.py +37 -0
  41. maxitor/repo_graph_port/git_error.py +36 -0
  42. maxitor/repo_graph_port/github_event.py +70 -0
  43. maxitor/repo_graph_port/graph_facts.py +35 -0
  44. maxitor/repo_graph_port/history.py +90 -0
  45. maxitor/repo_graph_port/history_context.py +45 -0
  46. maxitor/repo_graph_port/history_not_found_error.py +33 -0
  47. maxitor/repo_graph_port/history_point.py +36 -0
  48. maxitor/repo_graph_port/history_reader_info.py +36 -0
  49. maxitor/repo_graph_port/history_reader_role.py +37 -0
  50. maxitor/repo_graph_port/history_reading.py +382 -0
  51. maxitor/repo_graph_port/history_versions.py +187 -0
  52. maxitor/repo_graph_port/launch.py +62 -0
  53. maxitor/repo_graph_port/placed_tag.py +30 -0
  54. maxitor/repo_graph_port/py.typed +0 -0
  55. maxitor/repo_graph_port/repo_graph_domain.py +35 -0
  56. maxitor/repo_graph_port/resources/__init__.py +18 -0
  57. maxitor/repo_graph_port/resources/git_mirror.py +264 -0
  58. maxitor/repo_graph_port/resources/github_host.py +219 -0
  59. maxitor/repo_graph_port/resources/histories_config.py +226 -0
  60. maxitor/repo_graph_port/resources/version_cache.py +223 -0
  61. maxitor/repo_graph_port/security_reporter.py +54 -0
  62. maxitor/repo_graph_port/server.py +104 -0
  63. maxitor/repo_graph_port/tag.py +35 -0
  64. maxitor/repo_graph_port/too_many_wrong_keys_error.py +32 -0
  65. maxitor/repo_graph_port/version_file_middleware.py +75 -0
  66. maxitor/repo_graph_port/version_record.py +44 -0
  67. maxitor/repo_graph_port/version_source.py +67 -0
  68. maxitor/repo_graph_port/webhook_signature_middleware.py +79 -0
  69. maxitor/skills/aoa-graph-builder-setup/SKILL.md +123 -0
  70. maxitor/web/__init__.py +23 -0
  71. maxitor/web/api/__init__.py +16 -0
  72. maxitor/web/api/access_log.py +49 -0
  73. maxitor/web/api/app.py +524 -0
  74. maxitor/web/api/cached_static_files.py +82 -0
  75. maxitor/web/api/duckdb_graph_holder.py +109 -0
  76. maxitor/web/api/graph_source_middleware.py +294 -0
  77. maxitor/web/api/graph_store.py +317 -0
  78. maxitor/web/api/history_key_auth_coordinator.py +62 -0
  79. maxitor/web/api/main.py +32 -0
  80. maxitor/web/api/routes/__init__.py +6 -0
  81. maxitor/web/api/routes/load.py +32 -0
  82. maxitor/web/api/routes/sidebar.py +66 -0
  83. maxitor/web/api/services/__init__.py +6 -0
  84. maxitor/web/build_graph.py +39 -0
  85. maxitor/web/launch.py +51 -0
  86. maxitor/web/model/__init__.py +2 -0
  87. maxitor/web/model/core/__init__.py +10 -0
  88. maxitor/web/model/core/actions/__init__.py +16 -0
  89. maxitor/web/model/core/actions/app_info_action.py +81 -0
  90. maxitor/web/model/core/actions/check_due_sources_action.py +133 -0
  91. maxitor/web/model/core/actions/check_history_action.py +159 -0
  92. maxitor/web/model/core/actions/check_source_action.py +145 -0
  93. maxitor/web/model/core/actions/check_version_access_action.py +120 -0
  94. maxitor/web/model/core/actions/get_own_graph_json_action.py +73 -0
  95. maxitor/web/model/core/actions/graph_fingerprint_action.py +90 -0
  96. maxitor/web/model/core/actions/history_versions_action.py +117 -0
  97. maxitor/web/model/core/actions/left_sidebar_action.py +242 -0
  98. maxitor/web/model/core/actions/load_aoa_service_action.py +273 -0
  99. maxitor/web/model/core/actions/open_history_action.py +126 -0
  100. maxitor/web/model/core/core_domain.py +29 -0
  101. maxitor/web/model/core/entities/__init__.py +10 -0
  102. maxitor/web/model/core/entities/node_entity.py +43 -0
  103. maxitor/web/model/core/freshness.py +36 -0
  104. maxitor/web/model/core/history_address.py +238 -0
  105. maxitor/web/model/core/history_entry.py +145 -0
  106. maxitor/web/model/core/history_errors.py +60 -0
  107. maxitor/web/model/core/history_key_holder.py +40 -0
  108. maxitor/web/model/core/history_reading.py +170 -0
  109. maxitor/web/model/core/resources/__init__.py +42 -0
  110. maxitor/web/model/core/resources/graph_source_gateway.py +168 -0
  111. maxitor/web/model/core/resources/graph_store_controller.py +102 -0
  112. maxitor/web/model/core/resources/history_access.py +114 -0
  113. maxitor/web/model/core/resources/history_store.py +92 -0
  114. maxitor/web/model/core/resources/open_tabs_registry.py +62 -0
  115. maxitor/web/model/core/resources/own_model_graph_resource.py +63 -0
  116. maxitor/web/model/core/resources/repo_graph_port_gateway.py +176 -0
  117. maxitor/web/model/core/resources/source_checks.py +113 -0
  118. maxitor/web/model/core/resources/static_bundle_resource.py +72 -0
  119. maxitor/web/model/core/same_graph.py +49 -0
  120. maxitor/web/model/core/source_label.py +44 -0
  121. maxitor/web/model/diagrams/__init__.py +8 -0
  122. maxitor/web/model/diagrams/actions/__init__.py +20 -0
  123. maxitor/web/model/diagrams/actions/all_use_cases_diagram_action.py +72 -0
  124. maxitor/web/model/diagrams/actions/changed_inside_action.py +86 -0
  125. maxitor/web/model/diagrams/actions/compare_element_action.py +99 -0
  126. maxitor/web/model/diagrams/actions/compare_versions_action.py +111 -0
  127. maxitor/web/model/diagrams/actions/domain_use_case_diagram_action.py +355 -0
  128. maxitor/web/model/diagrams/actions/domain_use_case_diagram_action_schema.py +90 -0
  129. maxitor/web/model/diagrams/actions/events_explorer_action.py +157 -0
  130. maxitor/web/model/diagrams/actions/events_explorer_action_schema.py +32 -0
  131. maxitor/web/model/diagrams/actions/full_graph_action.py +271 -0
  132. maxitor/web/model/diagrams/actions/get_element_card_action.py +108 -0
  133. maxitor/web/model/diagrams/actions/get_lifecycle_finite_automaton_action.py +314 -0
  134. maxitor/web/model/diagrams/actions/get_lifecycle_finite_automaton_action_schema.py +84 -0
  135. maxitor/web/model/diagrams/actions/list_domains_action.py +167 -0
  136. maxitor/web/model/diagrams/actions/list_domains_action_schema.py +27 -0
  137. maxitor/web/model/diagrams/actions/list_entities_action.py +523 -0
  138. maxitor/web/model/diagrams/actions/list_entities_action_schema.py +230 -0
  139. maxitor/web/model/diagrams/actions/list_entity_kinds_action.py +108 -0
  140. maxitor/web/model/diagrams/actions/list_entity_kinds_action_schema.py +26 -0
  141. maxitor/web/model/diagrams/actions/list_node_types_action.py +175 -0
  142. maxitor/web/model/diagrams/actions/list_node_types_action_schema.py +26 -0
  143. maxitor/web/model/diagrams/actions/map_area_action.py +88 -0
  144. maxitor/web/model/diagrams/actions/map_changes_action.py +67 -0
  145. maxitor/web/model/diagrams/actions/map_overview_action.py +75 -0
  146. maxitor/web/model/diagrams/diagrams_domain.py +28 -0
  147. maxitor/web/model/diagrams/resources/__init__.py +24 -0
  148. maxitor/web/model/diagrams/resources/comparison_store.py +177 -0
  149. maxitor/web/model/diagrams/resources/duckdb_graph_resource.py +1264 -0
  150. maxitor/web/model/diagrams/resources/map_store.py +330 -0
  151. maxitor/web/model/rooms/__init__.py +15 -0
  152. maxitor/web/model/rooms/actions/__init__.py +41 -0
  153. maxitor/web/model/rooms/actions/disconnect_tab_action.py +103 -0
  154. maxitor/web/model/rooms/actions/end_room_action.py +90 -0
  155. maxitor/web/model/rooms/actions/expire_participant_action.py +94 -0
  156. maxitor/web/model/rooms/actions/follow_action.py +109 -0
  157. maxitor/web/model/rooms/actions/join_room_action.py +271 -0
  158. maxitor/web/model/rooms/actions/leave_room_action.py +86 -0
  159. maxitor/web/model/rooms/actions/reload_source_for_room_action.py +100 -0
  160. maxitor/web/model/rooms/actions/rename_participant_action.py +85 -0
  161. maxitor/web/model/rooms/actions/report_page_action.py +136 -0
  162. maxitor/web/model/rooms/actions/room_action_support.py +67 -0
  163. maxitor/web/model/rooms/actions/seed_page_settings_action.py +106 -0
  164. maxitor/web/model/rooms/actions/set_page_setting_action.py +93 -0
  165. maxitor/web/model/rooms/actions/set_room_version_action.py +127 -0
  166. maxitor/web/model/rooms/actions/stop_following_action.py +75 -0
  167. maxitor/web/model/rooms/follow_circle.py +65 -0
  168. maxitor/web/model/rooms/layout_authority.py +100 -0
  169. maxitor/web/model/rooms/pages.py +186 -0
  170. maxitor/web/model/rooms/resources/__init__.py +14 -0
  171. maxitor/web/model/rooms/resources/room_registry.py +585 -0
  172. maxitor/web/model/rooms/rooms_domain.py +29 -0
  173. maxitor/web/py.typed +0 -0
  174. maxitor/web/static/assets/index-CWo6xMfp.js +225 -0
  175. maxitor/web/static/assets/index-wUWjggHM.css +1 -0
  176. maxitor/web/static/assets/mapEngine-CLap2t01.js +3355 -0
  177. maxitor/web/static/build.json +1 -0
  178. maxitor/web/static/index.html +13 -0
  179. maxitor-3.11.0.dist-info/METADATA +205 -0
  180. maxitor-3.11.0.dist-info/RECORD +185 -0
  181. maxitor-3.11.0.dist-info/WHEEL +4 -0
  182. maxitor-3.11.0.dist-info/entry_points.txt +4 -0
  183. maxitor-3.11.0.dist-info/licenses/LICENSE +192 -0
  184. maxitor-3.11.0.dist-info/licenses/NOTICE +14 -0
  185. maxitor-3.11.0.dist-info/licenses/THIRD-PARTY-NOTICES.md +45 -0
@@ -0,0 +1,20 @@
1
+ # packages/maxitor/tools/graph_builder/maxitor/graph_builder/__init__.py
2
+ """
3
+ maxitor.graph_builder — the ``graph-builder`` command: a repository's graphs, kept in files.
4
+
5
+ ═══════════════════════════════════════════════════════════════════════════════
6
+ PURPOSE
7
+ ═══════════════════════════════════════════════════════════════════════════════
8
+
9
+ Keep each service's graph in one file, checked against the Open Gnomon Protocol
10
+ by version: the config that lists a repository's graphs, the writer and the
11
+ check, and the differences between a file and what its build prints.
12
+
13
+ ═══════════════════════════════════════════════════════════════════════════════
14
+ ARCHITECTURE / DATA FLOW
15
+ ═══════════════════════════════════════════════════════════════════════════════
16
+
17
+ The part imports nothing from ``aoa`` and nothing of ``maxitor.web``;
18
+ ``scripts/check_package_boundaries.py`` enforces both, so ``pip install maxitor``
19
+ with no extra is enough to run it.
20
+ """
@@ -0,0 +1,136 @@
1
+ # packages/maxitor/tools/graph_builder/maxitor/graph_builder/cli.py
2
+ """
3
+ The ``graph-builder`` command: ``write``, ``check``, ``validate`` and ``skill``.
4
+
5
+ ═══════════════════════════════════════════════════════════════════════════════
6
+ PURPOSE
7
+ ═══════════════════════════════════════════════════════════════════════════════
8
+
9
+ A developer runs ``graph-builder write`` and commits the graph files with the change;
10
+ CI runs ``graph-builder check`` and fails when a file is stale. ``validate`` tells
11
+ whether one document is a graph, ``skill`` prints the setup skill for agents.
12
+
13
+ ═══════════════════════════════════════════════════════════════════════════════
14
+ ARCHITECTURE / DATA FLOW
15
+ ═══════════════════════════════════════════════════════════════════════════════
16
+
17
+ ``read_config`` → ``writer.write`` / ``writer.check`` → one line per graph, the
18
+ lines that explain a failure, and the command that fixes it. Exit codes: 0 —
19
+ every graph is fine; 1 — a graph is not; 2 — the config was refused.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import argparse
25
+ import json
26
+ import os
27
+ import sys
28
+ from collections.abc import Callable, Sequence
29
+ from importlib.metadata import version
30
+ from importlib.resources import files
31
+ from pathlib import Path
32
+
33
+ from maxitor.graph_builder.config import ConfigRefused, Graph, read_config
34
+ from maxitor.graph_builder.schemas import GraphRefused, check_graph
35
+ from maxitor.graph_builder.writer import Outcome, check, write
36
+
37
+ DEFAULT_CONFIG = "graph-builder.toml"
38
+
39
+
40
+ def _shown_path(path: Path) -> str:
41
+ """A path as a person in the current folder would type it."""
42
+ relative = os.path.relpath(path)
43
+ return relative if not relative.startswith("..") else str(path)
44
+
45
+
46
+ def _line(outcome: Outcome) -> str:
47
+ """The one line that says what happened to a graph."""
48
+ name = outcome.graph.name
49
+ if outcome.state in ("written", "unchanged"):
50
+ return f"ok {name} ({outcome.state})"
51
+ if outcome.state == "current":
52
+ return f"ok {name}"
53
+ if outcome.state in ("stale", "missing"):
54
+ return f"{outcome.state} {name}: {_shown_path(outcome.graph.file)}"
55
+ return f"{outcome.state} {name}"
56
+
57
+
58
+ def _report(outcomes: list[Outcome], config: str) -> int:
59
+ """Print every outcome and, when one is not fine, the command that fixes it."""
60
+ for outcome in outcomes:
61
+ print(_line(outcome))
62
+ for detail in outcome.details:
63
+ print(f" {detail}")
64
+ broken = [o.graph.name for o in outcomes if o.state in ("failed", "refused")]
65
+ if broken:
66
+ print(f"fix the build of {', '.join(broken)}, then run: graph-builder write --config {config}")
67
+ elif not all(o.ok for o in outcomes):
68
+ print(f"run: graph-builder write --config {config}, and commit the graph files")
69
+ return 0 if all(o.ok for o in outcomes) else 1
70
+
71
+
72
+ def _run(config: str, action: Callable[[list[Graph]], list[Outcome]]) -> int:
73
+ """Read the config and report what ``action`` did with its graphs."""
74
+ try:
75
+ graphs = read_config(config)
76
+ except ConfigRefused as refused:
77
+ print(f"config refused: {config}: {refused}", file=sys.stderr)
78
+ return 2
79
+ return _report(action(graphs), config)
80
+
81
+
82
+ def _validate(source: str) -> int:
83
+ """Say whether one document — a file, or ``-`` for standard input — is a graph."""
84
+ try:
85
+ text = sys.stdin.read() if source == "-" else Path(source).read_text(encoding="utf-8")
86
+ except OSError as exc:
87
+ print(f"refused: cannot read {source}: {exc.strerror}")
88
+ return 1
89
+ try:
90
+ document = json.loads(text)
91
+ check_graph(document)
92
+ except ValueError as exc:
93
+ reason = str(exc) if isinstance(exc, GraphRefused) else f"not JSON ({exc})"
94
+ print(f"refused: {reason}")
95
+ return 1
96
+ print(f"ok {document['schema_version']}: {len(document['nodes'])} nodes, {len(document['edges'])} edges")
97
+ return 0
98
+
99
+
100
+ def _skill() -> int:
101
+ """Print the setup skill for coding agents, to be saved as ``<skills folder>/aoa-graph-builder-setup/SKILL.md``."""
102
+ sys.stdout.write(files("maxitor").joinpath("skills", "aoa-graph-builder-setup", "SKILL.md").read_text(encoding="utf-8"))
103
+ return 0
104
+
105
+
106
+ def main(argv: Sequence[str] | None = None) -> int:
107
+ """
108
+ AI-CORE-BEGIN
109
+ ROLE: The ``graph-builder`` console script.
110
+ CONTRACT: Returns the exit code: 0 — fine; 1 — a graph is not (stale, missing, failed, refused, not a graph); 2 — the config was refused.
111
+ AI-CORE-END
112
+ """
113
+ parser = argparse.ArgumentParser(prog="graph-builder", description="Keep each graph of a repository in its file.")
114
+ parser.add_argument("--version", action="version", version=f"graph-builder {version('maxitor')}")
115
+ commands = parser.add_subparsers(dest="command", required=True)
116
+ for name, about in (
117
+ ("write", "build every graph and write the files that changed"),
118
+ ("check", "build every graph and fail if a file is not what its build prints; writes nothing"),
119
+ ):
120
+ command = commands.add_parser(name, help=about, description=about)
121
+ command.add_argument("--config", default=DEFAULT_CONFIG, help=f"the config (default: {DEFAULT_CONFIG})")
122
+ validate = commands.add_parser("validate", help="say whether one document is a graph")
123
+ validate.add_argument("source", help="a graph file, or - for standard input")
124
+ commands.add_parser("skill", help="print the aoa-graph-builder-setup skill for coding agents")
125
+ args = parser.parse_args(argv)
126
+ if args.command == "write":
127
+ return _run(args.config, write)
128
+ if args.command == "check":
129
+ return _run(args.config, check)
130
+ if args.command == "validate":
131
+ return _validate(args.source)
132
+ return _skill()
133
+
134
+
135
+ if __name__ == "__main__":
136
+ raise SystemExit(main())
@@ -0,0 +1,141 @@
1
+ # packages/maxitor/tools/graph_builder/maxitor/graph_builder/config.py
2
+ """
3
+ Read ``graph-builder.toml``: the graphs of one repository.
4
+
5
+ ═══════════════════════════════════════════════════════════════════════════════
6
+ PURPOSE
7
+ ═══════════════════════════════════════════════════════════════════════════════
8
+
9
+ A repository lists its graphs in one file. Each ``[[graph]]`` names a graph, the
10
+ folder its build runs in, the shell command that prints it, and the file it is
11
+ saved in. This module reads that file into ``Graph`` values, or refuses it naming
12
+ the graph and the field that are wrong.
13
+
14
+ ═══════════════════════════════════════════════════════════════════════════════
15
+ ARCHITECTURE / DATA FLOW
16
+ ═══════════════════════════════════════════════════════════════════════════════
17
+
18
+ TOML → the config's own version (``graph_builder = "1.0"``) → its JSON Schema
19
+ (``config_schema.json`` beside this module, the one config version there is) → the rules a schema cannot say: names and
20
+ files unique → ``Graph`` values with paths resolved against the config's folder.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import tomllib
27
+ from dataclasses import dataclass
28
+ from functools import cache
29
+ from importlib.resources import files
30
+ from pathlib import Path
31
+ from typing import Any
32
+
33
+ from jsonschema import Draft202012Validator
34
+ from jsonschema.exceptions import ValidationError, best_match
35
+
36
+ CONFIG_VERSIONS: tuple[str, ...] = ("1.0",)
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class Graph:
41
+ """
42
+ AI-CORE-BEGIN
43
+ ROLE: One graph of a repository: what it is called, where and how it is built, where it is saved.
44
+ INVARIANTS: ``cwd`` and ``file`` are absolute, resolved against the config's folder; ``name`` and ``file`` are unique within one config.
45
+ AI-CORE-END
46
+ """
47
+
48
+ name: str
49
+ cwd: Path
50
+ build: str
51
+ file: Path
52
+
53
+
54
+ class ConfigRefused(ValueError):
55
+ """
56
+ AI-CORE-BEGIN
57
+ ROLE: A config refused: which graph, which field, and why.
58
+ CONTRACT: ``graph`` is the graph's name (``#n`` when it has none, ``None`` for the config itself); ``field`` is the field at fault, ``""`` for the whole file.
59
+ AI-CORE-END
60
+ """
61
+
62
+ def __init__(self, graph: str | None, field: str, reason: str) -> None:
63
+ """Keep the three parts and say them in one line."""
64
+ self.graph = graph
65
+ self.field = field
66
+ self.reason = reason
67
+ where = [f"graph {graph!r}"] if graph is not None else []
68
+ where += [field] if field else []
69
+ super().__init__(f"{', '.join(where)}: {reason}" if where else reason)
70
+
71
+
72
+ @cache
73
+ def _config_schema() -> dict[str, Any]:
74
+ """The JSON Schema of the config, version ``1.0``, read once."""
75
+ text = files("maxitor.graph_builder").joinpath("config_schema.json").read_text(encoding="utf-8")
76
+ schema: dict[str, Any] = json.loads(text)
77
+ return schema
78
+
79
+
80
+ def _graph_label(document: dict[str, Any], index: int) -> str:
81
+ """How a refusal names the graph at ``index``: its name, or its place when it has none."""
82
+ entry = document["graph"][index]
83
+ name = entry.get("name") if isinstance(entry, dict) else None
84
+ return name if isinstance(name, str) and name else f"#{index + 1}"
85
+
86
+
87
+ def _refusal(document: dict[str, Any], error: ValidationError) -> ConfigRefused:
88
+ """Turn the first schema error into a refusal that names the graph and the field."""
89
+ path = list(error.absolute_path)
90
+ in_graph = len(path) >= 2 and path[0] == "graph" and isinstance(path[1], int)
91
+ graph = _graph_label(document, int(path[1])) if in_graph else None
92
+ inner = path[2:] if graph is not None else path
93
+ instance, schema = error.instance, error.schema
94
+ if error.validator == "required" and isinstance(instance, dict) and isinstance(error.validator_value, list):
95
+ missing = [name for name in error.validator_value if name not in instance]
96
+ return ConfigRefused(graph, ".".join(map(str, [*inner, missing[0]])), "required")
97
+ if error.validator == "additionalProperties" and isinstance(instance, dict) and isinstance(schema, dict):
98
+ unknown = sorted(set(instance) - set(schema.get("properties", {})))
99
+ return ConfigRefused(graph, ".".join(map(str, [*inner, unknown[0]])), "not a field of the config")
100
+ return ConfigRefused(graph, ".".join(map(str, inner)), error.message)
101
+
102
+
103
+ def read_config(path: str | Path) -> list[Graph]:
104
+ """
105
+ AI-CORE-BEGIN
106
+ ROLE: Read a repository's ``graph-builder.toml`` into its graphs.
107
+ CONTRACT: Returns the graphs in file order, paths absolute; raises ``ConfigRefused`` naming the graph and the field — a missing file, not TOML, no or unknown ``graph-builder`` version, a missing or unknown field, a repeated name, two graphs with one file.
108
+ AI-CORE-END
109
+ """
110
+ config = Path(path)
111
+ try:
112
+ document = tomllib.loads(config.read_text(encoding="utf-8"))
113
+ except FileNotFoundError:
114
+ raise ConfigRefused(None, "", f"no config at {config}") from None
115
+ except tomllib.TOMLDecodeError as exc:
116
+ raise ConfigRefused(None, "", f"not TOML: {exc}") from None
117
+ version = document.get("graph_builder")
118
+ if version is None:
119
+ raise ConfigRefused(None, "graph_builder", 'required: the config\'s format version, graph_builder = "1.0"')
120
+ if version not in CONFIG_VERSIONS:
121
+ known = ", ".join(CONFIG_VERSIONS)
122
+ raise ConfigRefused(None, "graph_builder", f"unknown config version {version!r}; known: {known}")
123
+ error = best_match(Draft202012Validator(_config_schema()).iter_errors(document))
124
+ if error is not None:
125
+ raise _refusal(document, error)
126
+ base = config.resolve().parent
127
+ graphs: list[Graph] = []
128
+ for entry in document["graph"]:
129
+ graph = Graph(
130
+ name=entry["name"],
131
+ cwd=(base / entry.get("cwd", ".")).resolve(),
132
+ build=entry["build"],
133
+ file=(base / entry["file"]).resolve(),
134
+ )
135
+ for earlier in graphs:
136
+ if earlier.name == graph.name:
137
+ raise ConfigRefused(graph.name, "name", "another graph has this name")
138
+ if earlier.file == graph.file:
139
+ raise ConfigRefused(graph.name, "file", f"graph {earlier.name!r} is saved in this file too")
140
+ graphs.append(graph)
141
+ return graphs
@@ -0,0 +1,105 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/bystrovmaxim/aoa/main/packages/maxitor/tools/graph_builder/maxitor/graph_builder/config_schema.json",
4
+ "title": "graph-builder config",
5
+ "description": "graph-builder.toml: the graphs of one repository, each built by a command and saved in a file.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["graph_builder", "graph"],
9
+ "properties": {
10
+ "graph_builder": {
11
+ "const": "1.0"
12
+ },
13
+ "setup": {
14
+ "type": "object",
15
+ "additionalProperties": false,
16
+ "required": [
17
+ "skill",
18
+ "decided",
19
+ "deferred"
20
+ ],
21
+ "description": "Wizard decisions, not evidence of current health or a project release version.",
22
+ "properties": {
23
+ "skill": {
24
+ "type": "string",
25
+ "pattern": "^[1-9][0-9]*$"
26
+ },
27
+ "decided": {
28
+ "type": "array",
29
+ "uniqueItems": true,
30
+ "items": {
31
+ "type": "string",
32
+ "pattern": "^(config|local-update|ci-graph-check|version-tags|graph:.+|exporter:.+)$"
33
+ }
34
+ },
35
+ "deferred": {
36
+ "type": "array",
37
+ "uniqueItems": true,
38
+ "items": {
39
+ "enum": [
40
+ "local-update",
41
+ "ci-graph-check"
42
+ ]
43
+ }
44
+ }
45
+ },
46
+ "allOf": [
47
+ {
48
+ "if": {
49
+ "properties": {
50
+ "deferred": {
51
+ "contains": {
52
+ "const": "local-update"
53
+ }
54
+ }
55
+ }
56
+ },
57
+ "then": {
58
+ "properties": {
59
+ "decided": {
60
+ "contains": {
61
+ "const": "local-update"
62
+ }
63
+ }
64
+ }
65
+ }
66
+ },
67
+ {
68
+ "if": {
69
+ "properties": {
70
+ "deferred": {
71
+ "contains": {
72
+ "const": "ci-graph-check"
73
+ }
74
+ }
75
+ }
76
+ },
77
+ "then": {
78
+ "properties": {
79
+ "decided": {
80
+ "contains": {
81
+ "const": "ci-graph-check"
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+ ]
88
+ },
89
+ "graph": {
90
+ "type": "array",
91
+ "minItems": 1,
92
+ "items": {
93
+ "type": "object",
94
+ "additionalProperties": false,
95
+ "required": ["name", "build", "file"],
96
+ "properties": {
97
+ "name": {"type": "string", "minLength": 1},
98
+ "cwd": {"type": "string", "minLength": 1},
99
+ "build": {"type": "string", "minLength": 1},
100
+ "file": {"type": "string", "minLength": 1}
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,134 @@
1
+ # packages/maxitor/tools/graph_builder/maxitor/graph_builder/differences.py
2
+ """
3
+ Name what changed between two graph documents, by ``id``.
4
+
5
+ ═══════════════════════════════════════════════════════════════════════════════
6
+ PURPOSE
7
+ ═══════════════════════════════════════════════════════════════════════════════
8
+
9
+ When a saved graph is stale, a person needs to see what moved, not a text diff
10
+ of thousands of lines. Nodes and edges carry an ``id``, so an element is matched
11
+ by it wherever it sits in the file, and each change is one line.
12
+
13
+ ═══════════════════════════════════════════════════════════════════════════════
14
+ ARCHITECTURE / DATA FLOW
15
+ ═══════════════════════════════════════════════════════════════════════════════
16
+
17
+ ::
18
+
19
+ old, new ──► maxitor.graph_compare.compare ──► one line per element added or removed,
20
+ one per property of a changed one
21
+ └─ the other top-level keys (``schema_version``) ──► walked as plain JSON
22
+ └─ a document compare refuses ──► the whole of it walked as plain JSON
23
+
24
+ The comparison itself is ``maxitor.graph_compare``'s, the one Maxitor draws from:
25
+ a ``1.0`` graph's edges get their ids by #285's rule. Only a document that cannot be
26
+ compared — a broken file ``check`` still has to explain — falls back to a generic walk:
27
+ objects key by key, an array whose items all carry an ``id`` by ``id``, anything else
28
+ by equality.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import json
34
+ from collections.abc import Iterator, Mapping
35
+ from typing import Any
36
+
37
+ from maxitor.graph_compare import Change, Comparison, GraphNotComparableError, Status, compare
38
+
39
+ SHOWN_CHARACTERS = 60
40
+
41
+ _ELEMENTS = ("nodes", "edges")
42
+ """The top-level keys whose items the comparison matches by id."""
43
+
44
+
45
+ def _shown(value: Any) -> str:
46
+ """A value as one short line: text as it is, anything else as JSON."""
47
+ text = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, sort_keys=True)
48
+ return text if len(text) <= SHOWN_CHARACTERS else text[: SHOWN_CHARACTERS - 1] + "…"
49
+
50
+
51
+ def _by_id(items: list[Any]) -> dict[str, Any] | None:
52
+ """The items keyed by ``id`` when every item is an object with its own ``id``; otherwise ``None``."""
53
+ keyed: dict[str, Any] = {}
54
+ for item in items:
55
+ if not isinstance(item, dict) or not isinstance(item.get("id"), str) or item["id"] in keyed:
56
+ return None
57
+ keyed[item["id"]] = item
58
+ return keyed
59
+
60
+
61
+ def _walk(old: Any, new: Any, path: str) -> Iterator[str]:
62
+ """Every difference under ``path``, one line each, in a stable order — for what the comparison does not read."""
63
+ if isinstance(old, dict) and isinstance(new, dict):
64
+ for key in sorted(old.keys() | new.keys()):
65
+ inner = f"{path}.{key}" if path else key
66
+ if key not in new:
67
+ yield f"{inner}: removed"
68
+ elif key not in old:
69
+ yield f"{inner}: added"
70
+ else:
71
+ yield from _walk(old[key], new[key], inner)
72
+ return
73
+ if isinstance(old, list) and isinstance(new, list):
74
+ old_ids, new_ids = _by_id(old), _by_id(new)
75
+ if old_ids is not None and new_ids is not None:
76
+ for item_id in [*old_ids, *(i for i in new_ids if i not in old_ids)]:
77
+ inner = f"{path}[id={item_id}]"
78
+ if item_id not in new_ids:
79
+ yield f"{inner}: removed"
80
+ elif item_id not in old_ids:
81
+ yield f"{inner}: added"
82
+ else:
83
+ yield from _walk(old_ids[item_id], new_ids[item_id], inner)
84
+ return
85
+ if old != new:
86
+ yield f"{path}: {_shown(old)} → {_shown(new)}"
87
+
88
+
89
+ def _compared(comparison: Comparison, kind: str) -> Iterator[str]:
90
+ """One kind's lines from the comparison: an element added or removed, or each changed property by its path."""
91
+ for element_id, item in (comparison.nodes if kind == "nodes" else comparison.edges).items():
92
+ head = f"{kind}[id={element_id}]"
93
+ if item.status is Status.ADDED or item.status is Status.REMOVED:
94
+ yield f"{head}: {item.status.value}"
95
+ for difference in item.differences:
96
+ path = head + "".join(f".{key}" for key in difference.path)
97
+ if difference.change is Change.CHANGED:
98
+ yield f"{path}: {_shown(difference.before)} → {_shown(difference.after)}"
99
+ else:
100
+ yield f"{path}: {difference.change.value}"
101
+
102
+
103
+ def _lines(old: Any, new: Any) -> Iterator[str]:
104
+ """Every line, top-level keys in order: nodes and edges from the comparison, the rest walked."""
105
+ if not (isinstance(old, Mapping) and isinstance(new, Mapping)):
106
+ yield from _walk(old, new, "")
107
+ return
108
+ try:
109
+ comparison = compare(old, new)
110
+ except GraphNotComparableError:
111
+ yield from _walk(old, new, "")
112
+ return
113
+ for key in sorted(old.keys() | new.keys()):
114
+ if key in _ELEMENTS:
115
+ yield from _compared(comparison, key)
116
+ elif key not in new:
117
+ yield f"{key}: removed"
118
+ elif key not in old:
119
+ yield f"{key}: added"
120
+ else:
121
+ yield from _walk(old[key], new[key], key)
122
+
123
+
124
+ def differences(old: Any, new: Any, limit: int = 20) -> list[str]:
125
+ """
126
+ AI-CORE-BEGIN
127
+ ROLE: The differences between two graph documents, one line each, elements matched by ``id``.
128
+ CONTRACT: Returns ``[]`` when the documents are equal; otherwise at most ``limit`` lines, then ``… and N more``.
129
+ AI-CORE-END
130
+ """
131
+ lines = list(_lines(old, new))
132
+ if len(lines) > limit:
133
+ return [*lines[:limit], f"… and {len(lines) - limit} more"]
134
+ return lines
File without changes