archml 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- archml/__init__.py +6 -0
- archml/cli/__init__.py +4 -0
- archml/cli/main.py +614 -0
- archml/compiler/__init__.py +23 -0
- archml/compiler/artifact.py +76 -0
- archml/compiler/build.py +357 -0
- archml/compiler/parser.py +822 -0
- archml/compiler/scanner.py +393 -0
- archml/compiler/semantic_analysis.py +1087 -0
- archml/export/__init__.py +133 -0
- archml/model/__init__.py +52 -0
- archml/model/entities.py +205 -0
- archml/model/types.py +90 -0
- archml/sphinx_ext/__init__.py +13 -0
- archml/sphinx_ext/extension.py +402 -0
- archml/static/archml-diagram.css +619 -0
- archml/static/archml-viewer-template.html +90 -0
- archml/static/archml-viewer.js +78 -0
- archml/validation/__init__.py +18 -0
- archml/validation/checks.py +404 -0
- archml/views/__init__.py +4 -0
- archml/views/diagram.py +453 -0
- archml/views/layout.py +518 -0
- archml/views/placement.py +615 -0
- archml/views/resolver.py +104 -0
- archml/views/topology.py +1588 -0
- archml/workspace/__init__.py +48 -0
- archml/workspace/config.py +153 -0
- archml/workspace/git_ops.py +148 -0
- archml/workspace/lockfile.py +92 -0
- archml-0.1.0.dist-info/METADATA +475 -0
- archml-0.1.0.dist-info/RECORD +35 -0
- archml-0.1.0.dist-info/WHEEL +4 -0
- archml-0.1.0.dist-info/entry_points.txt +2 -0
- archml-0.1.0.dist-info/licenses/LICENSE +201 -0
archml/__init__.py
ADDED
archml/cli/__init__.py
ADDED
archml/cli/main.py
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
# Copyright 2026 ArchML Contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Entry point for the ArchML command-line interface."""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from archml.compiler.build import CompilerError, SourceImportKey, compile_files
|
|
12
|
+
from archml.validation.checks import validate
|
|
13
|
+
from archml.workspace.config import WorkspaceConfigError, load_workspace_config
|
|
14
|
+
|
|
15
|
+
# ###############
|
|
16
|
+
# Public Interface
|
|
17
|
+
# ###############
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> None:
|
|
21
|
+
"""Run the ArchML CLI."""
|
|
22
|
+
parser = argparse.ArgumentParser(
|
|
23
|
+
prog="archml",
|
|
24
|
+
description="ArchML — architecture modeling tool",
|
|
25
|
+
)
|
|
26
|
+
subparsers = parser.add_subparsers(dest="command", metavar="COMMAND")
|
|
27
|
+
|
|
28
|
+
# Shared parent parser for the workspace directory option, used by all
|
|
29
|
+
# subcommands that operate on an existing workspace.
|
|
30
|
+
_workspace_parent = argparse.ArgumentParser(add_help=False)
|
|
31
|
+
_workspace_parent.add_argument(
|
|
32
|
+
"--workspace",
|
|
33
|
+
"-C",
|
|
34
|
+
default=".",
|
|
35
|
+
metavar="DIR",
|
|
36
|
+
help="Directory containing the ArchML workspace (default: current directory)",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# init subcommand
|
|
40
|
+
init_parser = subparsers.add_parser(
|
|
41
|
+
"init",
|
|
42
|
+
help="Initialize a new ArchML workspace",
|
|
43
|
+
description="Create a new ArchML workspace in a repository.",
|
|
44
|
+
)
|
|
45
|
+
init_parser.add_argument(
|
|
46
|
+
"name",
|
|
47
|
+
help="Mnemonic name for the workspace source import",
|
|
48
|
+
)
|
|
49
|
+
init_parser.add_argument(
|
|
50
|
+
"workspace_dir",
|
|
51
|
+
help="Directory to initialize the workspace in",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# check subcommand
|
|
55
|
+
subparsers.add_parser(
|
|
56
|
+
"check",
|
|
57
|
+
parents=[_workspace_parent],
|
|
58
|
+
help="Check the consistency of the architecture",
|
|
59
|
+
description="Validate architecture files for consistency errors.",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# visualize subcommand
|
|
63
|
+
visualize_parser = subparsers.add_parser(
|
|
64
|
+
"visualize",
|
|
65
|
+
parents=[_workspace_parent],
|
|
66
|
+
help="Generate a diagram for a system or component",
|
|
67
|
+
description="Render a box diagram for the specified architecture entity.",
|
|
68
|
+
)
|
|
69
|
+
visualize_parser.add_argument(
|
|
70
|
+
"entity",
|
|
71
|
+
help="Entity path (e.g. 'SystemA' or 'SystemA::ComponentB'), or 'all' to visualize every top-level entity",
|
|
72
|
+
)
|
|
73
|
+
visualize_parser.add_argument(
|
|
74
|
+
"output",
|
|
75
|
+
help="Output file path for the rendered diagram (e.g. 'diagram.png')",
|
|
76
|
+
)
|
|
77
|
+
visualize_parser.add_argument(
|
|
78
|
+
"--depth",
|
|
79
|
+
type=int,
|
|
80
|
+
default=None,
|
|
81
|
+
metavar="N",
|
|
82
|
+
help=(
|
|
83
|
+
"Maximum nesting depth to expand. "
|
|
84
|
+
"For a single entity: 0 = root only, 1 = direct children, etc. "
|
|
85
|
+
"For 'all': 0 = top-level entities as opaque boxes, 1 = expanded one level, etc. "
|
|
86
|
+
"Omit for full depth (default)."
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# export subcommand
|
|
91
|
+
export_parser = subparsers.add_parser(
|
|
92
|
+
"export",
|
|
93
|
+
parents=[_workspace_parent],
|
|
94
|
+
help="Export the architecture as a standalone HTML viewer",
|
|
95
|
+
description="Generate a self-contained HTML file with the interactive architecture viewer.",
|
|
96
|
+
)
|
|
97
|
+
export_parser.add_argument(
|
|
98
|
+
"--output",
|
|
99
|
+
"-o",
|
|
100
|
+
default="architecture.html",
|
|
101
|
+
metavar="FILE",
|
|
102
|
+
help="Output file path (default: architecture.html)",
|
|
103
|
+
)
|
|
104
|
+
export_parser.add_argument(
|
|
105
|
+
"--width-optimized",
|
|
106
|
+
action="store_true",
|
|
107
|
+
default=False,
|
|
108
|
+
help=(
|
|
109
|
+
"Combine left and right sidebars into a single left sidebar and add a top bar "
|
|
110
|
+
"with a hamburger toggle. Saves horizontal space for narrow viewports."
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# sync-remote subcommand
|
|
115
|
+
subparsers.add_parser(
|
|
116
|
+
"sync-remote",
|
|
117
|
+
parents=[_workspace_parent],
|
|
118
|
+
help="Download configured remote git repositories",
|
|
119
|
+
description=(
|
|
120
|
+
"Download remote git repositories listed in the workspace configuration "
|
|
121
|
+
"to the configured sync directory at the commits pinned in the lockfile."
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# update-remote subcommand
|
|
126
|
+
subparsers.add_parser(
|
|
127
|
+
"update-remote",
|
|
128
|
+
parents=[_workspace_parent],
|
|
129
|
+
help="Update remote git repository commits in the lockfile",
|
|
130
|
+
description=(
|
|
131
|
+
"Resolve branch or tag references to their latest commit SHAs and "
|
|
132
|
+
"write the results to the lockfile. Commit-hash revisions are pinned as-is."
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
args = parser.parse_args()
|
|
137
|
+
if args.command is None:
|
|
138
|
+
parser.print_help()
|
|
139
|
+
sys.exit(0)
|
|
140
|
+
|
|
141
|
+
sys.exit(_dispatch(args))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ################
|
|
145
|
+
# Implementation
|
|
146
|
+
# ################
|
|
147
|
+
|
|
148
|
+
_DEFAULT_BUILD_DIR = ".archml-build"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _template_path() -> Path:
|
|
152
|
+
"""Return the path to the bundled viewer HTML template."""
|
|
153
|
+
return Path(__file__).parent.parent / "static" / "archml-viewer-template.html"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _dispatch(args: argparse.Namespace) -> int:
|
|
157
|
+
"""Dispatch to the appropriate subcommand handler."""
|
|
158
|
+
if args.command == "init":
|
|
159
|
+
return _cmd_init(args)
|
|
160
|
+
if args.command == "check":
|
|
161
|
+
return _cmd_check(args)
|
|
162
|
+
if args.command == "visualize":
|
|
163
|
+
return _cmd_visualize(args)
|
|
164
|
+
if args.command == "export":
|
|
165
|
+
return _cmd_export(args)
|
|
166
|
+
if args.command == "sync-remote":
|
|
167
|
+
return _cmd_sync_remote(args)
|
|
168
|
+
if args.command == "update-remote":
|
|
169
|
+
return _cmd_update_remote(args)
|
|
170
|
+
return 0
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _cmd_init(args: argparse.Namespace) -> int:
|
|
174
|
+
"""Handle the init subcommand."""
|
|
175
|
+
name = args.name
|
|
176
|
+
if not name:
|
|
177
|
+
print("Error: mnemonic name cannot be empty.", file=sys.stderr)
|
|
178
|
+
return 1
|
|
179
|
+
if not re.match(r"^[a-z][a-z0-9_-]*$", name):
|
|
180
|
+
print(
|
|
181
|
+
f"Error: invalid mnemonic name '{name}': must start with a lowercase letter "
|
|
182
|
+
"followed by lowercase letters, digits, hyphens, or underscores.",
|
|
183
|
+
file=sys.stderr,
|
|
184
|
+
)
|
|
185
|
+
return 1
|
|
186
|
+
|
|
187
|
+
workspace_dir = Path(args.workspace_dir).resolve()
|
|
188
|
+
workspace_dir.mkdir(parents=True, exist_ok=True)
|
|
189
|
+
|
|
190
|
+
workspace_yaml = workspace_dir / ".archml-workspace.yaml"
|
|
191
|
+
if workspace_yaml.exists():
|
|
192
|
+
print(
|
|
193
|
+
f"Error: workspace already exists at '{workspace_yaml}'.",
|
|
194
|
+
file=sys.stderr,
|
|
195
|
+
)
|
|
196
|
+
return 1
|
|
197
|
+
|
|
198
|
+
workspace_yaml.write_text(
|
|
199
|
+
f"name: {name}\nbuild-directory: {_DEFAULT_BUILD_DIR}\nsource-imports:\n - name: {name}\n local-path: .\n",
|
|
200
|
+
encoding="utf-8",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
print(f"Initialized ArchML workspace '{name}' at '{workspace_dir}'.")
|
|
204
|
+
return 0
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _cmd_check(args: argparse.Namespace) -> int:
|
|
208
|
+
"""Handle the check subcommand."""
|
|
209
|
+
from archml.workspace.config import GitPathImport, LocalPathImport, find_workspace_root
|
|
210
|
+
|
|
211
|
+
directory = Path(args.workspace).resolve()
|
|
212
|
+
|
|
213
|
+
if not directory.exists():
|
|
214
|
+
print(f"Error: directory '{directory}' does not exist.", file=sys.stderr)
|
|
215
|
+
return 1
|
|
216
|
+
|
|
217
|
+
workspace_yaml = directory / ".archml-workspace.yaml"
|
|
218
|
+
|
|
219
|
+
if not workspace_yaml.exists():
|
|
220
|
+
root = find_workspace_root(directory)
|
|
221
|
+
if root is None:
|
|
222
|
+
print(
|
|
223
|
+
f"Error: no ArchML workspace found at '{directory}' or any parent directory."
|
|
224
|
+
"Run 'archml init' to initialize a workspace.",
|
|
225
|
+
file=sys.stderr,
|
|
226
|
+
)
|
|
227
|
+
return 1
|
|
228
|
+
directory = root
|
|
229
|
+
workspace_yaml = directory / ".archml-workspace.yaml"
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
config = load_workspace_config(workspace_yaml)
|
|
233
|
+
except WorkspaceConfigError as exc:
|
|
234
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
235
|
+
return 1
|
|
236
|
+
|
|
237
|
+
build_dir = directory / config.build_directory
|
|
238
|
+
sync_dir = directory / config.remote_sync_directory
|
|
239
|
+
|
|
240
|
+
# Build the source import map: SourceImportKey(repo, mnemonic) -> absolute base path.
|
|
241
|
+
# Local mnemonics use config.name as repo; remote repos use "@name".
|
|
242
|
+
source_import_map: dict[SourceImportKey, Path] = {}
|
|
243
|
+
|
|
244
|
+
for imp in config.source_imports:
|
|
245
|
+
if isinstance(imp, LocalPathImport):
|
|
246
|
+
source_import_map[SourceImportKey(config.name, imp.name)] = (directory / imp.local_path).resolve()
|
|
247
|
+
elif isinstance(imp, GitPathImport):
|
|
248
|
+
repo_dir = (sync_dir / imp.name).resolve()
|
|
249
|
+
if repo_dir.exists():
|
|
250
|
+
remote_workspace_yaml = repo_dir / ".archml-workspace.yaml"
|
|
251
|
+
if remote_workspace_yaml.exists():
|
|
252
|
+
try:
|
|
253
|
+
remote_config = load_workspace_config(remote_workspace_yaml)
|
|
254
|
+
for remote_imp in remote_config.source_imports:
|
|
255
|
+
if isinstance(remote_imp, LocalPathImport):
|
|
256
|
+
mnemonic_path = (repo_dir / remote_imp.local_path).resolve()
|
|
257
|
+
source_import_map[SourceImportKey(f"@{imp.name}", remote_imp.name)] = mnemonic_path
|
|
258
|
+
except WorkspaceConfigError as exc:
|
|
259
|
+
print(f"Warning: could not load workspace config from remote '{imp.name}': {exc}")
|
|
260
|
+
|
|
261
|
+
# Scan only files under local mnemonic paths (repo == config.name, i.e. not remote).
|
|
262
|
+
local_mnemonic_paths = {base_path for key, base_path in source_import_map.items() if key.repo == config.name}
|
|
263
|
+
seen_files: set[Path] = set()
|
|
264
|
+
archml_files: list[Path] = []
|
|
265
|
+
for base_path in sorted(local_mnemonic_paths):
|
|
266
|
+
for f in base_path.rglob("*.archml"):
|
|
267
|
+
if f not in seen_files and build_dir not in f.parents and sync_dir not in f.parents:
|
|
268
|
+
seen_files.add(f)
|
|
269
|
+
archml_files.append(f)
|
|
270
|
+
|
|
271
|
+
if not archml_files:
|
|
272
|
+
print("No .archml files found in the workspace.")
|
|
273
|
+
return 0
|
|
274
|
+
|
|
275
|
+
print(f"Checking {len(archml_files)} architecture file(s)...")
|
|
276
|
+
try:
|
|
277
|
+
compiled = compile_files(archml_files, build_dir, source_import_map)
|
|
278
|
+
except CompilerError as exc:
|
|
279
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
280
|
+
return 1
|
|
281
|
+
|
|
282
|
+
has_errors = False
|
|
283
|
+
for arch_file in compiled.values():
|
|
284
|
+
result = validate(arch_file)
|
|
285
|
+
for warning in result.warnings:
|
|
286
|
+
print(f"Warning: {warning.message}")
|
|
287
|
+
for error in result.errors:
|
|
288
|
+
print(f"Error: {error.message}", file=sys.stderr)
|
|
289
|
+
has_errors = True
|
|
290
|
+
|
|
291
|
+
if has_errors:
|
|
292
|
+
return 1
|
|
293
|
+
|
|
294
|
+
print("No issues found.")
|
|
295
|
+
return 0
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _cmd_visualize(args: argparse.Namespace) -> int:
|
|
299
|
+
"""Handle the visualize subcommand."""
|
|
300
|
+
from archml.views.layout import compute_layout
|
|
301
|
+
from archml.views.resolver import EntityNotFoundError, resolve_entity
|
|
302
|
+
from archml.views.topology import build_viz_diagram, build_viz_diagram_all
|
|
303
|
+
from archml.workspace.config import LocalPathImport
|
|
304
|
+
|
|
305
|
+
directory = Path(args.workspace).resolve()
|
|
306
|
+
|
|
307
|
+
if not directory.exists():
|
|
308
|
+
print(f"Error: directory '{directory}' does not exist.", file=sys.stderr)
|
|
309
|
+
return 1
|
|
310
|
+
|
|
311
|
+
workspace_yaml = directory / ".archml-workspace.yaml"
|
|
312
|
+
|
|
313
|
+
if not workspace_yaml.exists():
|
|
314
|
+
print(
|
|
315
|
+
f"Error: no ArchML workspace found at '{directory}'. Run 'archml init' to initialize a workspace.",
|
|
316
|
+
file=sys.stderr,
|
|
317
|
+
)
|
|
318
|
+
return 1
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
config = load_workspace_config(workspace_yaml)
|
|
322
|
+
except WorkspaceConfigError as exc:
|
|
323
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
324
|
+
return 1
|
|
325
|
+
|
|
326
|
+
build_dir = directory / config.build_directory
|
|
327
|
+
|
|
328
|
+
source_import_map: dict[SourceImportKey, Path] = {}
|
|
329
|
+
for imp in config.source_imports:
|
|
330
|
+
if isinstance(imp, LocalPathImport):
|
|
331
|
+
source_import_map[SourceImportKey(config.name, imp.name)] = (directory / imp.local_path).resolve()
|
|
332
|
+
|
|
333
|
+
archml_files = [f for f in directory.rglob("*.archml") if build_dir not in f.parents]
|
|
334
|
+
if not archml_files:
|
|
335
|
+
print("No .archml files found in the workspace.", file=sys.stderr)
|
|
336
|
+
return 1
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
compiled = compile_files(archml_files, build_dir, source_import_map)
|
|
340
|
+
except CompilerError as exc:
|
|
341
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
342
|
+
return 1
|
|
343
|
+
|
|
344
|
+
output_path = Path(args.output)
|
|
345
|
+
|
|
346
|
+
depth: int | None = args.depth
|
|
347
|
+
|
|
348
|
+
if args.entity == "all":
|
|
349
|
+
viz_diagram = build_viz_diagram_all(compiled, depth=depth)
|
|
350
|
+
else:
|
|
351
|
+
try:
|
|
352
|
+
entity = resolve_entity(compiled, args.entity)
|
|
353
|
+
except EntityNotFoundError as exc:
|
|
354
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
355
|
+
return 1
|
|
356
|
+
global_connects = [c for af in compiled.values() for c in af.connects]
|
|
357
|
+
viz_diagram = build_viz_diagram(entity, depth=depth, global_connects=global_connects)
|
|
358
|
+
try:
|
|
359
|
+
layout_plan = compute_layout(viz_diagram)
|
|
360
|
+
except RuntimeError as exc:
|
|
361
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
362
|
+
return 1
|
|
363
|
+
|
|
364
|
+
from archml.views.diagram import render_diagram
|
|
365
|
+
|
|
366
|
+
render_diagram(viz_diagram, layout_plan, output_path)
|
|
367
|
+
|
|
368
|
+
print(f"Diagram written to '{output_path}'.")
|
|
369
|
+
return 0
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _cmd_export(args: argparse.Namespace) -> int:
|
|
373
|
+
"""Handle the export subcommand."""
|
|
374
|
+
from archml.export import build_viewer_payload
|
|
375
|
+
from archml.workspace.config import LocalPathImport
|
|
376
|
+
|
|
377
|
+
template_path = _template_path()
|
|
378
|
+
if not template_path.exists():
|
|
379
|
+
print(
|
|
380
|
+
"Warning: JS viewer not built. Run 'python tools/build_js.py' first.",
|
|
381
|
+
file=sys.stderr,
|
|
382
|
+
)
|
|
383
|
+
return 1
|
|
384
|
+
|
|
385
|
+
directory = Path(args.workspace).resolve()
|
|
386
|
+
|
|
387
|
+
if not directory.exists():
|
|
388
|
+
print(f"Error: directory '{directory}' does not exist.", file=sys.stderr)
|
|
389
|
+
return 1
|
|
390
|
+
|
|
391
|
+
workspace_yaml = directory / ".archml-workspace.yaml"
|
|
392
|
+
|
|
393
|
+
if not workspace_yaml.exists():
|
|
394
|
+
print(
|
|
395
|
+
f"Error: no ArchML workspace found at '{directory}'. Run 'archml init' to initialize a workspace.",
|
|
396
|
+
file=sys.stderr,
|
|
397
|
+
)
|
|
398
|
+
return 1
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
config = load_workspace_config(workspace_yaml)
|
|
402
|
+
except WorkspaceConfigError as exc:
|
|
403
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
404
|
+
return 1
|
|
405
|
+
|
|
406
|
+
build_dir = directory / config.build_directory
|
|
407
|
+
|
|
408
|
+
source_import_map: dict[SourceImportKey, Path] = {}
|
|
409
|
+
for imp in config.source_imports:
|
|
410
|
+
if isinstance(imp, LocalPathImport):
|
|
411
|
+
source_import_map[SourceImportKey(config.name, imp.name)] = (directory / imp.local_path).resolve()
|
|
412
|
+
|
|
413
|
+
archml_files = [f for f in directory.rglob("*.archml") if build_dir not in f.parents]
|
|
414
|
+
if not archml_files:
|
|
415
|
+
print("No .archml files found in the workspace.", file=sys.stderr)
|
|
416
|
+
return 1
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
compiled = compile_files(archml_files, build_dir, source_import_map)
|
|
420
|
+
except CompilerError as exc:
|
|
421
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
422
|
+
return 1
|
|
423
|
+
|
|
424
|
+
payload_json = build_viewer_payload(compiled, width_optimized=args.width_optimized)
|
|
425
|
+
|
|
426
|
+
template = template_path.read_text(encoding="utf-8")
|
|
427
|
+
data_tag = f'<script id="archml-data" type="application/json">{payload_json}</script>'
|
|
428
|
+
html = template.replace("<!-- ARCHML_DATA_PLACEHOLDER -->", data_tag)
|
|
429
|
+
|
|
430
|
+
output_path = Path(args.output)
|
|
431
|
+
output_path.write_text(html, encoding="utf-8")
|
|
432
|
+
print(f"Architecture viewer written to '{output_path}'.")
|
|
433
|
+
return 0
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _cmd_sync_remote(args: argparse.Namespace) -> int:
|
|
437
|
+
"""Handle the sync-remote subcommand."""
|
|
438
|
+
from archml.workspace.config import GitPathImport, find_workspace_root
|
|
439
|
+
from archml.workspace.git_ops import GitError, clone_at_commit, get_current_commit
|
|
440
|
+
from archml.workspace.lockfile import LOCKFILE_NAME, LockfileError, load_lockfile
|
|
441
|
+
|
|
442
|
+
directory = Path(args.workspace).resolve()
|
|
443
|
+
|
|
444
|
+
if not directory.exists():
|
|
445
|
+
print(f"Error: directory '{directory}' does not exist.", file=sys.stderr)
|
|
446
|
+
return 1
|
|
447
|
+
|
|
448
|
+
workspace_yaml = directory / ".archml-workspace.yaml"
|
|
449
|
+
if not workspace_yaml.exists():
|
|
450
|
+
root = find_workspace_root(directory)
|
|
451
|
+
if root is None:
|
|
452
|
+
print(
|
|
453
|
+
f"Error: no ArchML workspace found at '{directory}' or any parent directory."
|
|
454
|
+
"Run 'archml init' to initialize a workspace.",
|
|
455
|
+
file=sys.stderr,
|
|
456
|
+
)
|
|
457
|
+
return 1
|
|
458
|
+
directory = root
|
|
459
|
+
workspace_yaml = directory / ".archml-workspace.yaml"
|
|
460
|
+
|
|
461
|
+
try:
|
|
462
|
+
config = load_workspace_config(workspace_yaml)
|
|
463
|
+
except WorkspaceConfigError as exc:
|
|
464
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
465
|
+
return 1
|
|
466
|
+
|
|
467
|
+
git_imports = [imp for imp in config.source_imports if isinstance(imp, GitPathImport)]
|
|
468
|
+
if not git_imports:
|
|
469
|
+
print("No remote git repositories configured. Nothing to sync.")
|
|
470
|
+
return 0
|
|
471
|
+
|
|
472
|
+
lockfile_path = directory / LOCKFILE_NAME
|
|
473
|
+
if not lockfile_path.exists():
|
|
474
|
+
print(
|
|
475
|
+
"Error: lockfile not found. Run 'archml update-remote' to create the lockfile.",
|
|
476
|
+
file=sys.stderr,
|
|
477
|
+
)
|
|
478
|
+
return 1
|
|
479
|
+
|
|
480
|
+
try:
|
|
481
|
+
lockfile = load_lockfile(lockfile_path)
|
|
482
|
+
except LockfileError as exc:
|
|
483
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
484
|
+
return 1
|
|
485
|
+
|
|
486
|
+
locked_by_name = {entry.name: entry for entry in lockfile.locked_revisions}
|
|
487
|
+
sync_dir = directory / config.remote_sync_directory
|
|
488
|
+
|
|
489
|
+
has_errors = False
|
|
490
|
+
for imp in git_imports:
|
|
491
|
+
if imp.name not in locked_by_name:
|
|
492
|
+
print(
|
|
493
|
+
f"Error: '{imp.name}' is not in the lockfile. Run 'archml update-remote' first.",
|
|
494
|
+
file=sys.stderr,
|
|
495
|
+
)
|
|
496
|
+
has_errors = True
|
|
497
|
+
continue
|
|
498
|
+
|
|
499
|
+
pinned_commit = locked_by_name[imp.name].commit
|
|
500
|
+
target_dir = sync_dir / imp.name
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
current = get_current_commit(target_dir)
|
|
504
|
+
except GitError as exc:
|
|
505
|
+
print(f"Error: cannot check current state of '{imp.name}': {exc}", file=sys.stderr)
|
|
506
|
+
has_errors = True
|
|
507
|
+
continue
|
|
508
|
+
|
|
509
|
+
if current == pinned_commit:
|
|
510
|
+
print(f" {imp.name}: already at {pinned_commit[:8]}")
|
|
511
|
+
continue
|
|
512
|
+
|
|
513
|
+
print(f" {imp.name}: syncing to {pinned_commit[:8]}...")
|
|
514
|
+
try:
|
|
515
|
+
clone_at_commit(imp.git_repository, pinned_commit, target_dir)
|
|
516
|
+
print(f" {imp.name}: done.")
|
|
517
|
+
except GitError as exc:
|
|
518
|
+
print(f"Error: failed to sync '{imp.name}': {exc}", file=sys.stderr)
|
|
519
|
+
has_errors = True
|
|
520
|
+
|
|
521
|
+
return 1 if has_errors else 0
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _cmd_update_remote(args: argparse.Namespace) -> int:
|
|
525
|
+
"""Handle the update-remote subcommand."""
|
|
526
|
+
from archml.workspace.config import GitPathImport, find_workspace_root
|
|
527
|
+
from archml.workspace.git_ops import GitError, is_commit_hash, resolve_commit
|
|
528
|
+
from archml.workspace.lockfile import (
|
|
529
|
+
LOCKFILE_NAME,
|
|
530
|
+
LockedRevision,
|
|
531
|
+
Lockfile,
|
|
532
|
+
LockfileError,
|
|
533
|
+
load_lockfile,
|
|
534
|
+
save_lockfile,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
directory = Path(args.workspace).resolve()
|
|
538
|
+
|
|
539
|
+
if not directory.exists():
|
|
540
|
+
print(f"Error: directory '{directory}' does not exist.", file=sys.stderr)
|
|
541
|
+
return 1
|
|
542
|
+
|
|
543
|
+
workspace_yaml = directory / ".archml-workspace.yaml"
|
|
544
|
+
if not workspace_yaml.exists():
|
|
545
|
+
root = find_workspace_root(directory)
|
|
546
|
+
if root is None:
|
|
547
|
+
print(
|
|
548
|
+
f"Error: no ArchML workspace found at '{directory}' or any parent directory."
|
|
549
|
+
"Run 'archml init' to initialize a workspace.",
|
|
550
|
+
file=sys.stderr,
|
|
551
|
+
)
|
|
552
|
+
return 1
|
|
553
|
+
directory = root
|
|
554
|
+
workspace_yaml = directory / ".archml-workspace.yaml"
|
|
555
|
+
|
|
556
|
+
try:
|
|
557
|
+
config = load_workspace_config(workspace_yaml)
|
|
558
|
+
except WorkspaceConfigError as exc:
|
|
559
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
560
|
+
return 1
|
|
561
|
+
|
|
562
|
+
git_imports = [imp for imp in config.source_imports if isinstance(imp, GitPathImport)]
|
|
563
|
+
if not git_imports:
|
|
564
|
+
print("No remote git repositories configured. Nothing to update.")
|
|
565
|
+
return 0
|
|
566
|
+
|
|
567
|
+
lockfile_path = directory / LOCKFILE_NAME
|
|
568
|
+
if lockfile_path.exists():
|
|
569
|
+
try:
|
|
570
|
+
lockfile = load_lockfile(lockfile_path)
|
|
571
|
+
except LockfileError as exc:
|
|
572
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
573
|
+
return 1
|
|
574
|
+
else:
|
|
575
|
+
lockfile = Lockfile()
|
|
576
|
+
|
|
577
|
+
locked_by_name = {entry.name: entry for entry in lockfile.locked_revisions}
|
|
578
|
+
|
|
579
|
+
has_errors = False
|
|
580
|
+
for imp in git_imports:
|
|
581
|
+
if is_commit_hash(imp.revision):
|
|
582
|
+
commit = imp.revision
|
|
583
|
+
print(f" {imp.name}: pinned at {commit[:8]} (commit hash, no update needed)")
|
|
584
|
+
else:
|
|
585
|
+
print(f" {imp.name}: resolving '{imp.revision}'...")
|
|
586
|
+
try:
|
|
587
|
+
commit = resolve_commit(imp.git_repository, imp.revision)
|
|
588
|
+
print(f" {imp.name}: resolved to {commit[:8]}")
|
|
589
|
+
except GitError as exc:
|
|
590
|
+
print(f"Error: failed to resolve '{imp.name}': {exc}", file=sys.stderr)
|
|
591
|
+
has_errors = True
|
|
592
|
+
continue
|
|
593
|
+
|
|
594
|
+
locked_by_name[imp.name] = LockedRevision.model_validate(
|
|
595
|
+
{
|
|
596
|
+
"name": imp.name,
|
|
597
|
+
"git-repository": imp.git_repository,
|
|
598
|
+
"revision": imp.revision,
|
|
599
|
+
"commit": commit,
|
|
600
|
+
}
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
if has_errors:
|
|
604
|
+
return 1
|
|
605
|
+
|
|
606
|
+
lockfile.locked_revisions = list(locked_by_name.values())
|
|
607
|
+
try:
|
|
608
|
+
save_lockfile(lockfile, lockfile_path)
|
|
609
|
+
except LockfileError as exc:
|
|
610
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
611
|
+
return 1
|
|
612
|
+
|
|
613
|
+
print(f"Lockfile updated: {lockfile_path}")
|
|
614
|
+
return 0
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Copyright 2026 ArchML Contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Compiler pipeline for .archml files: scanning, parsing, and semantic analysis."""
|
|
5
|
+
|
|
6
|
+
from archml.compiler.artifact import ARTIFACT_SUFFIX, deserialize, read_artifact, serialize, write_artifact
|
|
7
|
+
from archml.compiler.build import CompilerError, compile_files
|
|
8
|
+
from archml.compiler.parser import ParseError, parse
|
|
9
|
+
from archml.compiler.semantic_analysis import SemanticError, analyze
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"parse",
|
|
13
|
+
"ParseError",
|
|
14
|
+
"analyze",
|
|
15
|
+
"SemanticError",
|
|
16
|
+
"serialize",
|
|
17
|
+
"deserialize",
|
|
18
|
+
"write_artifact",
|
|
19
|
+
"read_artifact",
|
|
20
|
+
"ARTIFACT_SUFFIX",
|
|
21
|
+
"compile_files",
|
|
22
|
+
"CompilerError",
|
|
23
|
+
]
|