gitcad 0.7.7__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.
- gitcad/bench/__init__.py +0 -0
- gitcad/bench/corpus.py +260 -0
- gitcad/bench/scorecard.py +176 -0
- gitcad/bridge.py +72 -0
- gitcad/convert.py +30 -0
- gitcad/explore.py +99 -0
- gitcad/fasteners.py +112 -0
- gitcad/init.py +134 -0
- gitcad/lots.py +136 -0
- gitcad/mcp/__init__.py +15 -0
- gitcad/mcp/server.py +1065 -0
- gitcad/merge3.py +289 -0
- gitcad/pcba.py +114 -0
- gitcad/release.py +223 -0
- gitcad/render.py +177 -0
- gitcad/requirements.py +204 -0
- gitcad/review.py +230 -0
- gitcad/viewer/__init__.py +19 -0
- gitcad/viewer/boardsvg.py +80 -0
- gitcad/viewer/page.py +577 -0
- gitcad/viewer/server.py +433 -0
- gitcad-0.7.7.dist-info/METADATA +70 -0
- gitcad-0.7.7.dist-info/RECORD +25 -0
- gitcad-0.7.7.dist-info/WHEEL +4 -0
- gitcad-0.7.7.dist-info/entry_points.txt +11 -0
gitcad/fasteners.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Fastener generation — Toolbox, agent-first (SW-map P6).
|
|
2
|
+
|
|
3
|
+
Every mounting hole already publishes a ``mech.bolt`` port (ADR-0008);
|
|
4
|
+
this module closes the loop: a parametric socket-head bolt FAMILY
|
|
5
|
+
(P1 parameters + P2 configurations — one document, every length) and a
|
|
6
|
+
generator that walks an assembly's unmated ``mech.bolt`` ports, adds a
|
|
7
|
+
correctly sized bolt instance at each, and MATES it — so the standard
|
|
8
|
+
assembly validation (position coincidence, type compatibility) is the
|
|
9
|
+
proof that every screw is where a screw belongs.
|
|
10
|
+
|
|
11
|
+
Sizing: the port's ``spec["thread"]`` ("M3") picks the diameter;
|
|
12
|
+
``spec["length"]`` or the caller's default picks the length. Ports with
|
|
13
|
+
no thread spec are reported, never guessed.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
|
|
20
|
+
from gitcad.document import Document, Feature
|
|
21
|
+
from gitcad.errors import GitcadError
|
|
22
|
+
from gitcad.part import Frame, Interface, PartManifest, Port
|
|
23
|
+
|
|
24
|
+
# ISO 4762 socket head cap screw proportions: head Ø = 1.5d, head height = d
|
|
25
|
+
_STD_LENGTHS = (4, 6, 8, 10, 12, 16, 20, 25, 30)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def bolt_family(d: float) -> Document:
|
|
29
|
+
"""A parametric socket-head cap screw as ONE document: parameters for
|
|
30
|
+
every dimension, one configuration per standard length (P1 + P2 doing
|
|
31
|
+
what Toolbox does with thousands of files)."""
|
|
32
|
+
doc = Document()
|
|
33
|
+
doc.set_parameter("d", d)
|
|
34
|
+
doc.set_parameter("L", _STD_LENGTHS[2])
|
|
35
|
+
doc.set_parameter("head_d", "=d*1.5")
|
|
36
|
+
doc.set_parameter("head_h", "=d")
|
|
37
|
+
shaft = doc.add(Feature(op="cylinder",
|
|
38
|
+
params={"radius": "=d/2", "height": "=L"}))
|
|
39
|
+
head = doc.add(Feature(op="cylinder",
|
|
40
|
+
params={"radius": "=head_d/2", "height": "=head_h"}))
|
|
41
|
+
head_up = doc.add(Feature(op="move", params={"translate": [0, 0, "=L"]},
|
|
42
|
+
inputs=[head]))
|
|
43
|
+
doc.add(Feature(op="boolean", params={"kind": "union"},
|
|
44
|
+
inputs=[shaft, head_up]))
|
|
45
|
+
for length in _STD_LENGTHS:
|
|
46
|
+
doc.set_configuration(f"M{d:g}x{length}", {"L": length})
|
|
47
|
+
return doc
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def bolt_part(thread: str, length: float) -> PartManifest:
|
|
51
|
+
"""The bolt as a part: envelope from its dimensions, one ``mech.bolt``
|
|
52
|
+
port at the head seat (origin, -z shaft) — the thing that mates into a
|
|
53
|
+
mounting hole's port."""
|
|
54
|
+
d = _thread_diameter(thread)
|
|
55
|
+
head_d, head_h = 1.5 * d, d
|
|
56
|
+
iface = Interface(
|
|
57
|
+
envelope={"origin": [-head_d / 2, -head_d / 2, 0],
|
|
58
|
+
"dx": head_d, "dy": head_d, "dz": length + head_h},
|
|
59
|
+
frames={"seat": Frame()},
|
|
60
|
+
ports={"seat": Port(name="seat", type="mech.bolt", frame="seat",
|
|
61
|
+
spec={"thread": thread, "length": length})},
|
|
62
|
+
properties={"standard": "ISO4762", "thread": thread, "length": length},
|
|
63
|
+
)
|
|
64
|
+
safe = f"{thread}x{length:g}".replace(".", "_")
|
|
65
|
+
return PartManifest(id=f"prt_bolt_{safe.lower()}", name=f"{thread}x{length:g} SHCS",
|
|
66
|
+
domain="mech", version="1.0.0", interface=iface,
|
|
67
|
+
body={"kind": "fastener", "standard": "ISO4762"})
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def generate_fasteners(assembly, *, default_length: float = 8.0) -> dict:
|
|
71
|
+
"""Populate every unmated ``mech.bolt`` port with a sized bolt instance
|
|
72
|
+
+ mate. Returns {"added": [...], "skipped": [...]} — skipped ports name
|
|
73
|
+
their reason (already mated / no thread spec), never silently."""
|
|
74
|
+
mated: set[tuple[str, str]] = set()
|
|
75
|
+
for m in assembly.mates:
|
|
76
|
+
(ia, pa), (ib, pb) = m.split()
|
|
77
|
+
mated.add((ia, pa))
|
|
78
|
+
mated.add((ib, pb))
|
|
79
|
+
|
|
80
|
+
added: list[dict] = []
|
|
81
|
+
skipped: list[dict] = []
|
|
82
|
+
for iname, inst in sorted(assembly.instances.items()):
|
|
83
|
+
if inst.part.body.get("kind") == "fastener":
|
|
84
|
+
continue
|
|
85
|
+
for pname, port in sorted(inst.part.interface.ports.items()):
|
|
86
|
+
if port.type != "mech.bolt":
|
|
87
|
+
continue
|
|
88
|
+
if (iname, pname) in mated:
|
|
89
|
+
skipped.append({"port": f"{iname}.{pname}", "reason": "already-mated"})
|
|
90
|
+
continue
|
|
91
|
+
thread = (port.spec or {}).get("thread")
|
|
92
|
+
if not thread:
|
|
93
|
+
skipped.append({"port": f"{iname}.{pname}", "reason": "no-thread-spec"})
|
|
94
|
+
continue
|
|
95
|
+
length = float((port.spec or {}).get("length", default_length))
|
|
96
|
+
part = bolt_part(thread, length)
|
|
97
|
+
bolt_name = f"bolt_{iname}_{pname}"
|
|
98
|
+
pos = inst.port_position(pname)
|
|
99
|
+
assembly.add(bolt_name, part, translate=pos)
|
|
100
|
+
assembly.mate(f"{bolt_name}.seat", f"{iname}.{pname}")
|
|
101
|
+
added.append({"instance": bolt_name, "port": f"{iname}.{pname}",
|
|
102
|
+
"thread": thread, "length": length,
|
|
103
|
+
"position": list(pos)})
|
|
104
|
+
return {"added": added, "skipped": skipped}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _thread_diameter(thread: str) -> float:
|
|
108
|
+
m = re.fullmatch(r"[Mm](\d+(?:\.\d+)?)", thread.strip())
|
|
109
|
+
if not m:
|
|
110
|
+
raise GitcadError(
|
|
111
|
+
f"unsupported thread spec {thread!r} (want metric 'M<d>', e.g. 'M3')")
|
|
112
|
+
return float(m.group(1))
|
gitcad/init.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""gitcad init — a new project in one command.
|
|
2
|
+
|
|
3
|
+
A gitcad project IS a git repo; this scaffolds the anatomy and seeds
|
|
4
|
+
``<name>.gitcad`` — THE project root: the top-level assembly manifest
|
|
5
|
+
whose instances are the product's parts, mechanical and electrical alike
|
|
6
|
+
(an assembly IS a part, ADR-0008; a board-backed part is an instance like
|
|
7
|
+
any other). Mech feature-tree models are ``.model`` files referenced from
|
|
8
|
+
a part's body — a model is how one part gets its shape, never the product.
|
|
9
|
+
|
|
10
|
+
Also wired: the semantic merge driver (.gitattributes + git config), a CI
|
|
11
|
+
workflow running the review gate and the requirements suite, and a README
|
|
12
|
+
that explains the layout to the next person who clones it.
|
|
13
|
+
|
|
14
|
+
Extensions are directory legibility only — every tool detects document
|
|
15
|
+
kind by CONTENT (the schema field), and the old *.json names remain
|
|
16
|
+
accepted everywhere, forever.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import subprocess
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from gitcad.canonical import canonical_json
|
|
25
|
+
from gitcad.errors import GitcadError
|
|
26
|
+
|
|
27
|
+
_GITATTRIBUTES = """\
|
|
28
|
+
# gitcad semantic merge (ADR-0016): features by stable id, schematic
|
|
29
|
+
# connectivity by pin — never line-level merges of canonical JSON.
|
|
30
|
+
*.gitcad merge=gitcad
|
|
31
|
+
*.model merge=gitcad
|
|
32
|
+
*.sch merge=gitcad
|
|
33
|
+
*.board merge=gitcad
|
|
34
|
+
*.part merge=gitcad
|
|
35
|
+
*.pcba merge=gitcad
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
_WORKFLOW = """\
|
|
39
|
+
name: gitcad
|
|
40
|
+
on: [push, pull_request]
|
|
41
|
+
jobs:
|
|
42
|
+
checks:
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
with: {fetch-depth: 0}
|
|
47
|
+
- uses: actions/setup-python@v5
|
|
48
|
+
with: {python-version: "3.12"}
|
|
49
|
+
- run: pip install gitcad
|
|
50
|
+
- name: requirements suite
|
|
51
|
+
run: gitcad-verify requirements.reqs
|
|
52
|
+
- name: review gate (PRs)
|
|
53
|
+
if: github.event_name == 'pull_request'
|
|
54
|
+
run: gitcad-review --base origin/${{ github.base_ref }}
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
_README = """\
|
|
58
|
+
# {name}
|
|
59
|
+
|
|
60
|
+
A [gitcad](https://gitcad.xyz) project — the repo IS the project.
|
|
61
|
+
|
|
62
|
+
| file | what it is |
|
|
63
|
+
|------|-----------|
|
|
64
|
+
| `{name}.gitcad` | THE project: the top-level assembly of every part, mechanical and electrical |
|
|
65
|
+
| `*.part` | part manifests (envelope / frames / typed ports; subassemblies are parts too) |
|
|
66
|
+
| `*.model` | mechanical feature-tree models (canonical text; geometry is a build artifact) |
|
|
67
|
+
| `*.pcba` | electrical assemblies — mechanical from outside, enter for the electrical workflow |
|
|
68
|
+
| `*.sch` | schematics — the electrical source of truth |
|
|
69
|
+
| `*.board` | board layouts |
|
|
70
|
+
| `requirements.reqs` | executable requirements — `gitcad-verify` runs them |
|
|
71
|
+
| `release-*/` | built artifacts + `*.lot` fab-lot provenance records |
|
|
72
|
+
|
|
73
|
+
Branch = design variant. PR = design review with physics
|
|
74
|
+
(`gitcad-review`). Tag + lot record = a physical production run.
|
|
75
|
+
Merges are semantic (`gitcad-merge`, wired in `.gitattributes`).
|
|
76
|
+
View locally: `gitcad-view <file>`.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def init_project(path: str, *, name: str | None = None) -> list[str]:
|
|
81
|
+
"""Scaffold a project at path (created if missing). Returns files written.
|
|
82
|
+
Refuses to overwrite anything — init is for new projects, not repair."""
|
|
83
|
+
root = Path(path)
|
|
84
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
project = name or root.resolve().name
|
|
86
|
+
|
|
87
|
+
from gitcad.part import Assembly, new_part_id
|
|
88
|
+
|
|
89
|
+
root_manifest = Assembly(project).to_manifest(new_part_id())
|
|
90
|
+
files = {
|
|
91
|
+
".gitattributes": _GITATTRIBUTES,
|
|
92
|
+
".github/workflows/gitcad.yml": _WORKFLOW,
|
|
93
|
+
"README.md": _README.format(name=project),
|
|
94
|
+
f"{project}.gitcad": root_manifest.dumps(),
|
|
95
|
+
"requirements.reqs": canonical_json(
|
|
96
|
+
{"schema": "gitcad/requirements@1", "requirements": []},
|
|
97
|
+
indent=2) + "\n",
|
|
98
|
+
}
|
|
99
|
+
existing = [rel for rel in files if (root / rel).exists()]
|
|
100
|
+
if existing:
|
|
101
|
+
raise GitcadError(f"refusing to overwrite existing files: {existing} "
|
|
102
|
+
"— gitcad init is for new projects")
|
|
103
|
+
|
|
104
|
+
written = []
|
|
105
|
+
for rel, content in files.items():
|
|
106
|
+
target = root / rel
|
|
107
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
target.write_text(content, encoding="utf-8", newline="\n")
|
|
109
|
+
written.append(rel)
|
|
110
|
+
|
|
111
|
+
if not (root / ".git").exists():
|
|
112
|
+
subprocess.run(["git", "-C", str(root), "init", "-q"], check=True)
|
|
113
|
+
# Wire the merge driver into this clone's config (gitattributes names it;
|
|
114
|
+
# config supplies the command — both halves are needed).
|
|
115
|
+
subprocess.run(["git", "-C", str(root), "config",
|
|
116
|
+
"merge.gitcad.name", "gitcad semantic merge"], check=True)
|
|
117
|
+
subprocess.run(["git", "-C", str(root), "config",
|
|
118
|
+
"merge.gitcad.driver", "gitcad-merge %O %A %B"], check=True)
|
|
119
|
+
return written
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def main() -> None: # pragma: no cover - CLI entrypoint
|
|
123
|
+
import argparse
|
|
124
|
+
|
|
125
|
+
ap = argparse.ArgumentParser(
|
|
126
|
+
description="gitcad init — scaffold a new project (the repo IS the project)")
|
|
127
|
+
ap.add_argument("path", nargs="?", default=".")
|
|
128
|
+
ap.add_argument("--name", help="project name (default: directory name)")
|
|
129
|
+
args = ap.parse_args()
|
|
130
|
+
written = init_project(args.path, name=args.name)
|
|
131
|
+
print("initialized gitcad project:")
|
|
132
|
+
for rel in written:
|
|
133
|
+
print(f" {rel}")
|
|
134
|
+
print("merge driver wired (merge.gitcad in git config)")
|
gitcad/lots.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Fab-lot traceability — git bisect for hardware bugs.
|
|
2
|
+
|
|
3
|
+
A lot record binds a PHYSICAL build (fab order, date, vendor) to the exact
|
|
4
|
+
release that produced it: the git commit, the release manifest, and the
|
|
5
|
+
sha256 of every artifact that went to the fab. Years later, "units from
|
|
6
|
+
lot 7" resolves to a commit — and a field failure becomes::
|
|
7
|
+
|
|
8
|
+
git bisect start <bad-lot-commit> <good-lot-commit>
|
|
9
|
+
git bisect run gitcad-verify requirements.json
|
|
10
|
+
|
|
11
|
+
with the executing requirements suite as the oracle. PLM systems charge
|
|
12
|
+
six figures to approximate this badly; here it is a hash-pinned JSON file
|
|
13
|
+
in the repo.
|
|
14
|
+
|
|
15
|
+
``verify_lot`` re-hashes the artifacts on disk against the record — a
|
|
16
|
+
swapped Gerber can never silently claim a lot's provenance.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
import json
|
|
23
|
+
import subprocess
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from gitcad.canonical import canonical_json
|
|
27
|
+
from gitcad.errors import GitcadError
|
|
28
|
+
|
|
29
|
+
SCHEMA = "gitcad/lot@1"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _sha(path: Path) -> str:
|
|
33
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _head_commit(repo: Path) -> str:
|
|
37
|
+
proc = subprocess.run(["git", "-C", str(repo), "rev-parse", "HEAD"],
|
|
38
|
+
capture_output=True, text=True)
|
|
39
|
+
if proc.returncode != 0:
|
|
40
|
+
raise GitcadError("lot records need a git repo — provenance IS the point")
|
|
41
|
+
return proc.stdout.strip()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _dirty(repo: Path) -> bool:
|
|
45
|
+
proc = subprocess.run(["git", "-C", str(repo), "status", "--porcelain"],
|
|
46
|
+
capture_output=True, text=True)
|
|
47
|
+
return bool(proc.stdout.strip())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def record_lot(release_dir: str, lot_id: str, *, vendor: str = "",
|
|
51
|
+
date: str = "", quantity: int | None = None,
|
|
52
|
+
notes: str = "", repo: str = ".") -> str:
|
|
53
|
+
"""Create ``lot-<id>.json`` next to the release manifest. Refuses a
|
|
54
|
+
dirty worktree — a lot pinned to a commit that doesn't contain what
|
|
55
|
+
was actually sent would be provenance theater."""
|
|
56
|
+
root = Path(repo)
|
|
57
|
+
rel = Path(release_dir)
|
|
58
|
+
manifest = rel / "release-manifest.json"
|
|
59
|
+
if not manifest.is_file():
|
|
60
|
+
# fall back to any *-manifest.json in the release dir
|
|
61
|
+
candidates = sorted(rel.glob("*manifest*.json"))
|
|
62
|
+
if not candidates:
|
|
63
|
+
raise GitcadError(f"no release manifest found in {release_dir!r}")
|
|
64
|
+
manifest = candidates[0]
|
|
65
|
+
if _dirty(root):
|
|
66
|
+
raise GitcadError(
|
|
67
|
+
"worktree is dirty — commit first; a lot must pin a commit that "
|
|
68
|
+
"contains exactly what was sent to the fab")
|
|
69
|
+
|
|
70
|
+
artifacts = {p.name: _sha(p) for p in sorted(rel.iterdir())
|
|
71
|
+
if p.is_file() and p.suffix != ".lot"
|
|
72
|
+
and not p.name.startswith("lot-")}
|
|
73
|
+
doc = {"schema": SCHEMA, "lot": {
|
|
74
|
+
"id": lot_id, "vendor": vendor, "date": date,
|
|
75
|
+
**({"quantity": quantity} if quantity is not None else {}),
|
|
76
|
+
**({"notes": notes} if notes else {}),
|
|
77
|
+
"commit": _head_commit(root),
|
|
78
|
+
"manifest": manifest.name,
|
|
79
|
+
"artifacts": artifacts,
|
|
80
|
+
}}
|
|
81
|
+
out = rel / f"{lot_id}.lot"
|
|
82
|
+
if out.exists():
|
|
83
|
+
raise GitcadError(f"lot {lot_id!r} already recorded — lots are immutable; "
|
|
84
|
+
"record a new lot id for a re-run")
|
|
85
|
+
out.write_text(canonical_json(doc, indent=2) + "\n", encoding="utf-8")
|
|
86
|
+
return str(out)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def verify_lot(lot_path: str) -> dict:
|
|
90
|
+
"""Re-hash the artifacts against the lot record. Any mismatch is named —
|
|
91
|
+
a swapped file can never silently claim this lot's provenance."""
|
|
92
|
+
p = Path(lot_path)
|
|
93
|
+
doc = json.loads(p.read_text(encoding="utf-8"))
|
|
94
|
+
if doc.get("schema") != SCHEMA:
|
|
95
|
+
raise GitcadError(f"unsupported lot schema {doc.get('schema')!r}")
|
|
96
|
+
lot = doc["lot"]
|
|
97
|
+
mismatches: list[str] = []
|
|
98
|
+
missing: list[str] = []
|
|
99
|
+
for name, want in sorted(lot["artifacts"].items()):
|
|
100
|
+
f = p.parent / name
|
|
101
|
+
if not f.is_file():
|
|
102
|
+
missing.append(name)
|
|
103
|
+
elif _sha(f) != want:
|
|
104
|
+
mismatches.append(name)
|
|
105
|
+
ok = not (mismatches or missing)
|
|
106
|
+
return {"ok": ok, "lot": lot["id"], "commit": lot["commit"],
|
|
107
|
+
"artifacts": len(lot["artifacts"]),
|
|
108
|
+
"mismatched": mismatches, "missing": missing}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def main() -> None: # pragma: no cover - CLI entrypoint
|
|
112
|
+
import argparse
|
|
113
|
+
import sys
|
|
114
|
+
|
|
115
|
+
ap = argparse.ArgumentParser(description="gitcad lots — fab-lot provenance")
|
|
116
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
117
|
+
rec = sub.add_parser("record", help="record a lot against a release dir")
|
|
118
|
+
rec.add_argument("release_dir")
|
|
119
|
+
rec.add_argument("lot_id")
|
|
120
|
+
rec.add_argument("--vendor", default="")
|
|
121
|
+
rec.add_argument("--date", default="")
|
|
122
|
+
rec.add_argument("--quantity", type=int)
|
|
123
|
+
rec.add_argument("--notes", default="")
|
|
124
|
+
rec.add_argument("--repo", default=".")
|
|
125
|
+
ver = sub.add_parser("verify", help="re-hash artifacts against a lot record")
|
|
126
|
+
ver.add_argument("lot_file")
|
|
127
|
+
args = ap.parse_args()
|
|
128
|
+
if args.cmd == "record":
|
|
129
|
+
path = record_lot(args.release_dir, args.lot_id, vendor=args.vendor,
|
|
130
|
+
date=args.date, quantity=args.quantity,
|
|
131
|
+
notes=args.notes, repo=args.repo)
|
|
132
|
+
print(f"recorded {path}")
|
|
133
|
+
else:
|
|
134
|
+
r = verify_lot(args.lot_file)
|
|
135
|
+
print(json.dumps(r, indent=2))
|
|
136
|
+
sys.exit(0 if r["ok"] else 1)
|
gitcad/mcp/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""The MCP surface — gitcad's PRIMARY interface.
|
|
2
|
+
|
|
3
|
+
Design rule (ADR-0002): the MCP tool surface is designed first and the Python
|
|
4
|
+
API is a thin binding over the same handlers, not the reverse. If MCP is the
|
|
5
|
+
primary interface it never rots, because it is the interface everything (agents,
|
|
6
|
+
the web UI, humans) actually uses.
|
|
7
|
+
|
|
8
|
+
:mod:`gitcad.mcp.server` defines the tool handlers as plain, importable
|
|
9
|
+
functions (a registry) so they are unit-testable with no ``mcp`` dependency, and
|
|
10
|
+
wraps them as MCP tools only when the optional ``mcp`` package is present.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from gitcad.mcp.server import REGISTRY, main
|
|
14
|
+
|
|
15
|
+
__all__ = ["REGISTRY", "main"]
|