enforce-cpg 0.2.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.
enforce_cpg/LICENSE ADDED
@@ -0,0 +1,85 @@
1
+ Attribution Assurance License (AAL)
2
+
3
+ Copyright (c) 2026 tommyz4
4
+ https://github.com/tommyz4/enforce-cpg
5
+
6
+ The following is the full, official text of the Open Source Initiative (OSI)
7
+ approved Attribution Assurance License (SPDX identifier: AAL), as published
8
+ at https://opensource.org/license/aal, with the license's own attribution
9
+ fields filled in for this project: Author = tommyz4; Professional
10
+ Identification = enforce-cpg maintainer; URL = https://github.com/tommyz4/enforce-cpg.
11
+
12
+ --------------------------------------------------------------------------
13
+
14
+ Copyright (c) 2026 tommyz4
15
+
16
+ tommyz4, enforce-cpg maintainer * https://github.com/tommyz4/enforce-cpg
17
+
18
+ "enforce-cpg: Code Property Graph MCP server for DayZ Enforce Script"
19
+
20
+ All Rights Reserved
21
+
22
+ ATTRIBUTION ASSURANCE LICENSE (adapted from the original BSD license)
23
+
24
+ Redistribution and use in source and binary forms, with or without
25
+ modification, are permitted provided that the conditions below are met.
26
+
27
+ These conditions require a modest attribution to tommyz4 (the "Author"), who
28
+ hopes that its promotional value may help justify the thousands of dollars in
29
+ otherwise billable time invested in writing this and other freely available,
30
+ open-source software.
31
+
32
+ 1. Redistributions of source code, in whole or part and with or without
33
+ modification (the "Code"), must prominently display this GPG-signed text in
34
+ verifiable form.
35
+
36
+ 2. Redistributions of the Code in binary form must be accompanied by this
37
+ GPG-signed text in any documentation and, each time the resulting executable
38
+ program or a program dependent thereon is launched, a prominent display
39
+ (e.g., splash screen or banner text) of the Author's attribution
40
+ information, which includes:
41
+
42
+ (a) Name ("tommyz4"),
43
+
44
+ (b) Professional identification ("enforce-cpg maintainer"), and
45
+
46
+ (c) URL ("https://github.com/tommyz4/enforce-cpg").
47
+
48
+ 3. Neither the name nor any trademark of the Author may be used to endorse
49
+ or promote products derived from this software without specific prior
50
+ written permission.
51
+
52
+ 4. Users are entirely responsible, to the exclusion of the Author and any
53
+ other persons, for compliance with
54
+
55
+ (1) regulations set by owners or administrators of employed equipment,
56
+
57
+ (2) licensing terms of any other software, and
58
+
59
+ (3) local regulations regarding use, including those regarding import,
60
+ export, and use of encryption software.
61
+
62
+ THIS FREE SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
63
+ IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
64
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
65
+ EVENT SHALL THE AUTHOR OR ANY CONTRIBUTOR BE LIABLE FOR ANY DIRECT,
66
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
67
+ (INCLUDING, BUT NOT LIMITED TO, EFFECTS OF UNAUTHORIZED OR MALICIOUS NETWORK
68
+ ACCESS; PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
69
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
70
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
71
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
72
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
73
+
74
+ --End of License
75
+
76
+ Originally written by Edwin A. Suominen for licensing his PRIVARIA secure
77
+ networking software (see www.privaria.org). The author, who is not an
78
+ attorney, places this license template into the public domain along with a
79
+ complete disclaimer of any warranty or responsibility for its content or
80
+ legal efficacy. You may use or modify the language freely, but entirely at
81
+ your own risk.
82
+
83
+ --------------------------------------------------------------------------
84
+
85
+ Source: https://opensource.org/license/aal (fetched 2026-07-24).
enforce_cpg/NOTICE ADDED
@@ -0,0 +1,9 @@
1
+ enforce-cpg
2
+ Copyright (c) 2026 tommyz4
3
+ https://github.com/tommyz4/enforce-cpg
4
+
5
+ This product includes software developed by tommyz4.
6
+ Licensed under the Attribution Assurance License (AAL).
7
+
8
+ Attribution Notice (must be displayed per AAL clause):
9
+ enforce-cpg — © 2026 tommyz4 — https://github.com/tommyz4/enforce-cpg
@@ -0,0 +1,12 @@
1
+ from importlib.metadata import PackageNotFoundError
2
+ from importlib.metadata import version as _pkg_version
3
+
4
+ try:
5
+ __version__ = _pkg_version("enforce-cpg")
6
+ except PackageNotFoundError: # not installed (e.g. running from a raw source tree)
7
+ __version__ = "0.0.0+unknown"
8
+
9
+ ATTRIBUTION = (
10
+ "enforce-cpg — © 2026 tommyz4 — "
11
+ "https://github.com/tommyz4/enforce-cpg (AAL)"
12
+ )
enforce_cpg/cli.py ADDED
@@ -0,0 +1,93 @@
1
+ """enforce-cpg CLI: index / find-symbol / overview / audit / mod-surface / doctor / ingest."""
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+
7
+ from . import ATTRIBUTION, __version__, query
8
+ from .config import load_config
9
+ from .index import build_index
10
+ from .store import Store
11
+
12
+
13
+ def main(argv=None) -> int:
14
+ p = argparse.ArgumentParser(prog="enforce-cpg")
15
+ p.add_argument("--version", action="store_true")
16
+ sub = p.add_subparsers(dest="cmd")
17
+
18
+ pi = sub.add_parser("index")
19
+ pi.add_argument("--config", default="enforce-cpg.toml")
20
+ pi.add_argument("--full", action="store_true")
21
+
22
+ pr = sub.add_parser("reindex")
23
+ pr.add_argument("--config", default="enforce-cpg.toml")
24
+ pr.add_argument("--full", action="store_true")
25
+
26
+ pf = sub.add_parser("find-symbol")
27
+ pf.add_argument("name")
28
+ pf.add_argument("--kind")
29
+ pf.add_argument("--config", default="enforce-cpg.toml")
30
+
31
+ po = sub.add_parser("overview")
32
+ po.add_argument("--file")
33
+ po.add_argument("--class", dest="cls")
34
+ po.add_argument("--config", default="enforce-cpg.toml")
35
+
36
+ pa = sub.add_parser("audit")
37
+ pa.add_argument("--config", default="enforce-cpg.toml")
38
+
39
+ pm = sub.add_parser("mod-surface")
40
+ pm.add_argument("--source", required=True)
41
+ pm.add_argument("--config", default="enforce-cpg.toml")
42
+
43
+ pd = sub.add_parser("doctor")
44
+ pd.add_argument("--config", default="enforce-cpg.toml")
45
+ pd.add_argument("--json", action="store_true", help="Emit the raw report as JSON.")
46
+
47
+ pin = sub.add_parser("ingest")
48
+ pin.add_argument("input", help="A .pbo file or an already-extracted source directory.")
49
+ pin.add_argument("--name", required=True)
50
+ pin.add_argument("--kind", choices=["core", "mod"], required=True)
51
+ pin.add_argument("--after", help="Insert after this existing source (load order).")
52
+ pin.add_argument("--config", default="enforce-cpg.toml")
53
+
54
+ args = p.parse_args(argv)
55
+ if args.version:
56
+ print(f"enforce-cpg {__version__}\n{ATTRIBUTION}")
57
+ return 0
58
+ if args.cmd == "index":
59
+ cfg = load_config(Path(args.config))
60
+ print(ATTRIBUTION)
61
+ print(json.dumps(build_index(cfg, full=args.full)))
62
+ return 0
63
+ if args.cmd == "reindex":
64
+ cfg = load_config(Path(args.config))
65
+ print(ATTRIBUTION)
66
+ print(json.dumps(build_index(cfg, full=args.full)))
67
+ return 0
68
+ if args.cmd == "doctor":
69
+ from . import doctor
70
+ cfg = load_config(Path(args.config))
71
+ report = doctor.run(cfg)
72
+ print(json.dumps(report, indent=2) if args.json else doctor.format_report(report))
73
+ return 0
74
+ if args.cmd == "ingest":
75
+ from . import ingest as ingest_mod
76
+ res = ingest_mod.ingest(args.config, args.input, args.name, args.kind, args.after)
77
+ print(json.dumps(res, indent=2))
78
+ return 0
79
+ if args.cmd in ("find-symbol", "overview", "audit", "mod-surface"):
80
+ cfg = load_config(Path(args.config))
81
+ st = Store(cfg.db_path)
82
+ if args.cmd == "find-symbol":
83
+ result = query.find_symbol(st, args.name, args.kind)
84
+ elif args.cmd == "overview":
85
+ result = query.get_overview(st, file=args.file, cls=args.cls)
86
+ elif args.cmd == "audit":
87
+ result = query.audit_overrides(st)
88
+ else:
89
+ result = query.mod_surface(st, args.source)
90
+ print(json.dumps(result, indent=2))
91
+ return 0
92
+ p.print_help()
93
+ return 1
enforce_cpg/config.py ADDED
@@ -0,0 +1,34 @@
1
+ import tomllib
2
+ from dataclasses import dataclass, field
3
+ from pathlib import Path
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class Source:
8
+ name: str
9
+ kind: str
10
+ path: Path
11
+ load_order: int
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Config:
16
+ name: str
17
+ db_path: Path
18
+ sources: list[Source]
19
+ exclude: list[str] = field(default_factory=list)
20
+
21
+
22
+ def load_config(path: Path) -> Config:
23
+ data = tomllib.loads(Path(path).read_text())
24
+ project = data.get("project", {})
25
+ sources = [
26
+ Source(s["name"], s.get("kind", "mod"), Path(s["path"]), i)
27
+ for i, s in enumerate(data.get("sources", []))
28
+ ]
29
+ return Config(
30
+ name=project.get("name", "unnamed"),
31
+ db_path=Path(project.get("db_path", ".enforce-cpg/index.db")),
32
+ sources=sources,
33
+ exclude=list(project.get("exclude", [])),
34
+ )
@@ -0,0 +1,46 @@
1
+ import fnmatch
2
+ import re
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from .config import Source
7
+
8
+ _LAYER_RE = re.compile(r"^([1-5])_(\w+)$")
9
+
10
+ # Directory names that never contain Enforce Script and whose stray *.c files
11
+ # (vendored C extensions, tooling) would otherwise become noise diagnostics.
12
+ _DEFAULT_EXCLUDE_DIRS = (
13
+ ".venv", "venv", "node_modules", ".git", ".svn", "__pycache__", ".enforce-cpg",
14
+ )
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class FileEntry:
19
+ path: Path
20
+ source_name: str
21
+ layer: int
22
+ layer_name: str
23
+
24
+
25
+ def _layer_for(path: Path, root: Path) -> tuple[int, str]:
26
+ for part in path.relative_to(root).parts:
27
+ m = _LAYER_RE.match(part)
28
+ if m:
29
+ return int(m.group(1)), part
30
+ return 0, ""
31
+
32
+
33
+ def discover(sources: list[Source], exclude: list[str] = ()) -> list[FileEntry]:
34
+ """Find every ``*.c`` under each source root, skipping any file whose path
35
+ passes through an excluded directory. ``_DEFAULT_EXCLUDE_DIRS`` is always
36
+ applied; ``exclude`` adds project-configured dir-name globs on top."""
37
+ patterns = _DEFAULT_EXCLUDE_DIRS + tuple(exclude)
38
+ entries: list[FileEntry] = []
39
+ for src in sources:
40
+ for c in sorted(src.path.rglob("*.c")):
41
+ dir_parts = c.relative_to(src.path).parts[:-1]
42
+ if any(fnmatch.fnmatch(part, pat) for part in dir_parts for pat in patterns):
43
+ continue
44
+ layer, layer_name = _layer_for(c, src.path)
45
+ entries.append(FileEntry(c, src.name, layer, layer_name))
46
+ return entries
enforce_cpg/doctor.py ADDED
@@ -0,0 +1,103 @@
1
+ """Setup + index-health report (issue #8).
2
+
3
+ ``run(cfg)`` returns a structured report the CLI and MCP tool render: which
4
+ sources resolved and their live parse coverage, whether a vanilla (core) source
5
+ is configured, a diagnostics breakdown, and the unresolved-reference hotspots
6
+ that usually mean a source still needs ingesting. Read-only; never raises on a
7
+ bad file (parse failures are counted, not propagated).
8
+ """
9
+
10
+ from collections import Counter
11
+ from pathlib import Path
12
+
13
+ from .config import Config
14
+ from .discovery import discover
15
+ from .parser import parse
16
+ from .store import Store
17
+
18
+
19
+ def _parse_coverage(entries) -> tuple[int, int]:
20
+ ok = err = 0
21
+ for e in entries:
22
+ try:
23
+ if parse(e.path.read_bytes()).root_node.has_error:
24
+ err += 1
25
+ else:
26
+ ok += 1
27
+ except Exception: # noqa: BLE001 — coverage probe must never crash
28
+ err += 1
29
+ return ok, err
30
+
31
+
32
+ def run(cfg: Config) -> dict:
33
+ report: dict = {"project": cfg.name, "warnings": [], "sources": [], "index": {}}
34
+
35
+ if not any(s.kind == "core" for s in cfg.sources):
36
+ report["warnings"].append(
37
+ "No core (vanilla) source configured — inheritance/override chains "
38
+ "will dangle at base classes. Extract the game's scripts.pbo and add "
39
+ 'a [[sources]] kind="core" entry (see docs/vanilla-extraction.md).'
40
+ )
41
+
42
+ for s in cfg.sources:
43
+ entries = discover([s], cfg.exclude)
44
+ ok, err = _parse_coverage(entries)
45
+ total = ok + err
46
+ report["sources"].append({
47
+ "name": s.name,
48
+ "kind": s.kind,
49
+ "path": str(s.path),
50
+ "resolved": s.path.exists(),
51
+ "load_order": s.load_order,
52
+ "files": total,
53
+ "parse_ok": ok,
54
+ "parse_rate": round(ok / total, 4) if total else None,
55
+ })
56
+
57
+ db = Path(cfg.db_path)
58
+ if not db.exists():
59
+ report["index"] = {"indexed": False,
60
+ "note": "not indexed yet — run `enforce-cpg index --full`"}
61
+ return report
62
+
63
+ store = Store(cfg.db_path)
64
+ diags = store.diagnostics()
65
+ raws: Counter = Counter()
66
+ for e in store.edges():
67
+ if e.dst_id is None:
68
+ raw = e.attrs.get("raw")
69
+ if raw:
70
+ raws[raw] += 1
71
+ report["index"] = {
72
+ "indexed": True,
73
+ "nodes": len(store.nodes()),
74
+ "diagnostics": dict(Counter(d["kind"] for d in diags)),
75
+ "diagnostic_files_top": [f for f, _ in Counter(d["file"] for d in diags).most_common(10)],
76
+ "unresolved_top": raws.most_common(10),
77
+ }
78
+ return report
79
+
80
+
81
+ def format_report(report: dict) -> str:
82
+ """Human-readable summary of ``run``'s dict for the CLI."""
83
+ lines = [f"enforce-cpg doctor — project '{report['project']}'", ""]
84
+ for w in report["warnings"]:
85
+ lines.append(f" ⚠ {w}")
86
+ if report["warnings"]:
87
+ lines.append("")
88
+ lines.append("Sources (load order):")
89
+ for s in report["sources"]:
90
+ rate = "n/a" if s["parse_rate"] is None else f"{s['parse_rate']:.1%}"
91
+ mark = "ok" if s["resolved"] else "MISSING"
92
+ lines.append(f" [{s['load_order']}] {s['name']} ({s['kind']}) — {mark}, "
93
+ f"{s['files']} files, parse {rate}")
94
+ idx = report["index"]
95
+ lines.append("")
96
+ if not idx.get("indexed"):
97
+ lines.append(f"Index: {idx['note']}")
98
+ else:
99
+ lines.append(f"Index: {idx['nodes']} nodes; diagnostics {idx['diagnostics']}")
100
+ if idx["unresolved_top"]:
101
+ top = ", ".join(f"{n}×{c}" for n, c in idx["unresolved_top"][:5])
102
+ lines.append(f" Top unresolved refs (ingest a source that defines these?): {top}")
103
+ return "\n".join(lines)