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.
- maxitor/graph_builder/__init__.py +20 -0
- maxitor/graph_builder/cli.py +136 -0
- maxitor/graph_builder/config.py +141 -0
- maxitor/graph_builder/config_schema.json +105 -0
- maxitor/graph_builder/differences.py +134 -0
- maxitor/graph_builder/py.typed +0 -0
- maxitor/graph_builder/schemas/1.0.json +1476 -0
- maxitor/graph_builder/schemas/1.1.json +1481 -0
- maxitor/graph_builder/schemas/1.2.json +1518 -0
- maxitor/graph_builder/schemas/1.3.json +1600 -0
- maxitor/graph_builder/schemas/__init__.py +154 -0
- maxitor/graph_builder/writer.py +182 -0
- maxitor/graph_compare/__init__.py +50 -0
- maxitor/graph_compare/changed_inside.py +141 -0
- maxitor/graph_compare/compare.py +150 -0
- maxitor/graph_compare/comparison.py +143 -0
- maxitor/graph_compare/edge_ids.py +78 -0
- maxitor/graph_compare/py.typed +0 -0
- maxitor/graph_map/__init__.py +50 -0
- maxitor/graph_map/areas.py +272 -0
- maxitor/graph_map/assemble.py +314 -0
- maxitor/graph_map/layout.py +67 -0
- maxitor/graph_map/places.py +117 -0
- maxitor/graph_map/py.typed +0 -0
- maxitor/graph_map/union.py +88 -0
- maxitor/repo_graph_port/__init__.py +23 -0
- maxitor/repo_graph_port/actions/__init__.py +17 -0
- maxitor/repo_graph_port/actions/describe_history_action.py +113 -0
- maxitor/repo_graph_port/actions/history_action.py +157 -0
- maxitor/repo_graph_port/actions/receive_host_event_action.py +119 -0
- maxitor/repo_graph_port/actions/report_security_event_action.py +89 -0
- maxitor/repo_graph_port/actions/version_action.py +106 -0
- maxitor/repo_graph_port/auth/__init__.py +19 -0
- maxitor/repo_graph_port/auth/history_key_auth_coordinator.py +104 -0
- maxitor/repo_graph_port/auth/history_key_authenticator.py +58 -0
- maxitor/repo_graph_port/auth/history_key_credential_extractor.py +38 -0
- maxitor/repo_graph_port/build_graph.py +35 -0
- maxitor/repo_graph_port/cli.py +207 -0
- maxitor/repo_graph_port/commit.py +35 -0
- maxitor/repo_graph_port/contribution.py +37 -0
- maxitor/repo_graph_port/git_error.py +36 -0
- maxitor/repo_graph_port/github_event.py +70 -0
- maxitor/repo_graph_port/graph_facts.py +35 -0
- maxitor/repo_graph_port/history.py +90 -0
- maxitor/repo_graph_port/history_context.py +45 -0
- maxitor/repo_graph_port/history_not_found_error.py +33 -0
- maxitor/repo_graph_port/history_point.py +36 -0
- maxitor/repo_graph_port/history_reader_info.py +36 -0
- maxitor/repo_graph_port/history_reader_role.py +37 -0
- maxitor/repo_graph_port/history_reading.py +382 -0
- maxitor/repo_graph_port/history_versions.py +187 -0
- maxitor/repo_graph_port/launch.py +62 -0
- maxitor/repo_graph_port/placed_tag.py +30 -0
- maxitor/repo_graph_port/py.typed +0 -0
- maxitor/repo_graph_port/repo_graph_domain.py +35 -0
- maxitor/repo_graph_port/resources/__init__.py +18 -0
- maxitor/repo_graph_port/resources/git_mirror.py +264 -0
- maxitor/repo_graph_port/resources/github_host.py +219 -0
- maxitor/repo_graph_port/resources/histories_config.py +226 -0
- maxitor/repo_graph_port/resources/version_cache.py +223 -0
- maxitor/repo_graph_port/security_reporter.py +54 -0
- maxitor/repo_graph_port/server.py +104 -0
- maxitor/repo_graph_port/tag.py +35 -0
- maxitor/repo_graph_port/too_many_wrong_keys_error.py +32 -0
- maxitor/repo_graph_port/version_file_middleware.py +75 -0
- maxitor/repo_graph_port/version_record.py +44 -0
- maxitor/repo_graph_port/version_source.py +67 -0
- maxitor/repo_graph_port/webhook_signature_middleware.py +79 -0
- maxitor/skills/aoa-graph-builder-setup/SKILL.md +123 -0
- maxitor/web/__init__.py +23 -0
- maxitor/web/api/__init__.py +16 -0
- maxitor/web/api/access_log.py +49 -0
- maxitor/web/api/app.py +524 -0
- maxitor/web/api/cached_static_files.py +82 -0
- maxitor/web/api/duckdb_graph_holder.py +109 -0
- maxitor/web/api/graph_source_middleware.py +294 -0
- maxitor/web/api/graph_store.py +317 -0
- maxitor/web/api/history_key_auth_coordinator.py +62 -0
- maxitor/web/api/main.py +32 -0
- maxitor/web/api/routes/__init__.py +6 -0
- maxitor/web/api/routes/load.py +32 -0
- maxitor/web/api/routes/sidebar.py +66 -0
- maxitor/web/api/services/__init__.py +6 -0
- maxitor/web/build_graph.py +39 -0
- maxitor/web/launch.py +51 -0
- maxitor/web/model/__init__.py +2 -0
- maxitor/web/model/core/__init__.py +10 -0
- maxitor/web/model/core/actions/__init__.py +16 -0
- maxitor/web/model/core/actions/app_info_action.py +81 -0
- maxitor/web/model/core/actions/check_due_sources_action.py +133 -0
- maxitor/web/model/core/actions/check_history_action.py +159 -0
- maxitor/web/model/core/actions/check_source_action.py +145 -0
- maxitor/web/model/core/actions/check_version_access_action.py +120 -0
- maxitor/web/model/core/actions/get_own_graph_json_action.py +73 -0
- maxitor/web/model/core/actions/graph_fingerprint_action.py +90 -0
- maxitor/web/model/core/actions/history_versions_action.py +117 -0
- maxitor/web/model/core/actions/left_sidebar_action.py +242 -0
- maxitor/web/model/core/actions/load_aoa_service_action.py +273 -0
- maxitor/web/model/core/actions/open_history_action.py +126 -0
- maxitor/web/model/core/core_domain.py +29 -0
- maxitor/web/model/core/entities/__init__.py +10 -0
- maxitor/web/model/core/entities/node_entity.py +43 -0
- maxitor/web/model/core/freshness.py +36 -0
- maxitor/web/model/core/history_address.py +238 -0
- maxitor/web/model/core/history_entry.py +145 -0
- maxitor/web/model/core/history_errors.py +60 -0
- maxitor/web/model/core/history_key_holder.py +40 -0
- maxitor/web/model/core/history_reading.py +170 -0
- maxitor/web/model/core/resources/__init__.py +42 -0
- maxitor/web/model/core/resources/graph_source_gateway.py +168 -0
- maxitor/web/model/core/resources/graph_store_controller.py +102 -0
- maxitor/web/model/core/resources/history_access.py +114 -0
- maxitor/web/model/core/resources/history_store.py +92 -0
- maxitor/web/model/core/resources/open_tabs_registry.py +62 -0
- maxitor/web/model/core/resources/own_model_graph_resource.py +63 -0
- maxitor/web/model/core/resources/repo_graph_port_gateway.py +176 -0
- maxitor/web/model/core/resources/source_checks.py +113 -0
- maxitor/web/model/core/resources/static_bundle_resource.py +72 -0
- maxitor/web/model/core/same_graph.py +49 -0
- maxitor/web/model/core/source_label.py +44 -0
- maxitor/web/model/diagrams/__init__.py +8 -0
- maxitor/web/model/diagrams/actions/__init__.py +20 -0
- maxitor/web/model/diagrams/actions/all_use_cases_diagram_action.py +72 -0
- maxitor/web/model/diagrams/actions/changed_inside_action.py +86 -0
- maxitor/web/model/diagrams/actions/compare_element_action.py +99 -0
- maxitor/web/model/diagrams/actions/compare_versions_action.py +111 -0
- maxitor/web/model/diagrams/actions/domain_use_case_diagram_action.py +355 -0
- maxitor/web/model/diagrams/actions/domain_use_case_diagram_action_schema.py +90 -0
- maxitor/web/model/diagrams/actions/events_explorer_action.py +157 -0
- maxitor/web/model/diagrams/actions/events_explorer_action_schema.py +32 -0
- maxitor/web/model/diagrams/actions/full_graph_action.py +271 -0
- maxitor/web/model/diagrams/actions/get_element_card_action.py +108 -0
- maxitor/web/model/diagrams/actions/get_lifecycle_finite_automaton_action.py +314 -0
- maxitor/web/model/diagrams/actions/get_lifecycle_finite_automaton_action_schema.py +84 -0
- maxitor/web/model/diagrams/actions/list_domains_action.py +167 -0
- maxitor/web/model/diagrams/actions/list_domains_action_schema.py +27 -0
- maxitor/web/model/diagrams/actions/list_entities_action.py +523 -0
- maxitor/web/model/diagrams/actions/list_entities_action_schema.py +230 -0
- maxitor/web/model/diagrams/actions/list_entity_kinds_action.py +108 -0
- maxitor/web/model/diagrams/actions/list_entity_kinds_action_schema.py +26 -0
- maxitor/web/model/diagrams/actions/list_node_types_action.py +175 -0
- maxitor/web/model/diagrams/actions/list_node_types_action_schema.py +26 -0
- maxitor/web/model/diagrams/actions/map_area_action.py +88 -0
- maxitor/web/model/diagrams/actions/map_changes_action.py +67 -0
- maxitor/web/model/diagrams/actions/map_overview_action.py +75 -0
- maxitor/web/model/diagrams/diagrams_domain.py +28 -0
- maxitor/web/model/diagrams/resources/__init__.py +24 -0
- maxitor/web/model/diagrams/resources/comparison_store.py +177 -0
- maxitor/web/model/diagrams/resources/duckdb_graph_resource.py +1264 -0
- maxitor/web/model/diagrams/resources/map_store.py +330 -0
- maxitor/web/model/rooms/__init__.py +15 -0
- maxitor/web/model/rooms/actions/__init__.py +41 -0
- maxitor/web/model/rooms/actions/disconnect_tab_action.py +103 -0
- maxitor/web/model/rooms/actions/end_room_action.py +90 -0
- maxitor/web/model/rooms/actions/expire_participant_action.py +94 -0
- maxitor/web/model/rooms/actions/follow_action.py +109 -0
- maxitor/web/model/rooms/actions/join_room_action.py +271 -0
- maxitor/web/model/rooms/actions/leave_room_action.py +86 -0
- maxitor/web/model/rooms/actions/reload_source_for_room_action.py +100 -0
- maxitor/web/model/rooms/actions/rename_participant_action.py +85 -0
- maxitor/web/model/rooms/actions/report_page_action.py +136 -0
- maxitor/web/model/rooms/actions/room_action_support.py +67 -0
- maxitor/web/model/rooms/actions/seed_page_settings_action.py +106 -0
- maxitor/web/model/rooms/actions/set_page_setting_action.py +93 -0
- maxitor/web/model/rooms/actions/set_room_version_action.py +127 -0
- maxitor/web/model/rooms/actions/stop_following_action.py +75 -0
- maxitor/web/model/rooms/follow_circle.py +65 -0
- maxitor/web/model/rooms/layout_authority.py +100 -0
- maxitor/web/model/rooms/pages.py +186 -0
- maxitor/web/model/rooms/resources/__init__.py +14 -0
- maxitor/web/model/rooms/resources/room_registry.py +585 -0
- maxitor/web/model/rooms/rooms_domain.py +29 -0
- maxitor/web/py.typed +0 -0
- maxitor/web/static/assets/index-CWo6xMfp.js +225 -0
- maxitor/web/static/assets/index-wUWjggHM.css +1 -0
- maxitor/web/static/assets/mapEngine-CLap2t01.js +3355 -0
- maxitor/web/static/build.json +1 -0
- maxitor/web/static/index.html +13 -0
- maxitor-3.11.0.dist-info/METADATA +205 -0
- maxitor-3.11.0.dist-info/RECORD +185 -0
- maxitor-3.11.0.dist-info/WHEEL +4 -0
- maxitor-3.11.0.dist-info/entry_points.txt +4 -0
- maxitor-3.11.0.dist-info/licenses/LICENSE +192 -0
- maxitor-3.11.0.dist-info/licenses/NOTICE +14 -0
- 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
|