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 +85 -0
- enforce_cpg/NOTICE +9 -0
- enforce_cpg/__init__.py +12 -0
- enforce_cpg/cli.py +93 -0
- enforce_cpg/config.py +34 -0
- enforce_cpg/discovery.py +46 -0
- enforce_cpg/doctor.py +103 -0
- enforce_cpg/extract.py +333 -0
- enforce_cpg/index.py +63 -0
- enforce_cpg/ingest.py +105 -0
- enforce_cpg/mcp_server.py +257 -0
- enforce_cpg/model.py +27 -0
- enforce_cpg/nodetypes.py +50 -0
- enforce_cpg/parser.py +28 -0
- enforce_cpg/query.py +226 -0
- enforce_cpg/resolve.py +208 -0
- enforce_cpg/store.py +133 -0
- enforce_cpg-0.2.0.dist-info/METADATA +141 -0
- enforce_cpg-0.2.0.dist-info/RECORD +23 -0
- enforce_cpg-0.2.0.dist-info/WHEEL +4 -0
- enforce_cpg-0.2.0.dist-info/entry_points.txt +3 -0
- enforce_cpg-0.2.0.dist-info/licenses/LICENSE +85 -0
- enforce_cpg-0.2.0.dist-info/licenses/NOTICE +9 -0
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
|
enforce_cpg/__init__.py
ADDED
|
@@ -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
|
+
)
|
enforce_cpg/discovery.py
ADDED
|
@@ -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)
|