ngksbuildcore 0.1.4__tar.gz

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.
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.4
2
+ Name: ngksbuildcore
3
+ Version: 0.1.4
4
+ Summary: NGKsBuildCore
5
+ Author: NGKsSystems
6
+ License: Proprietary
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+
10
+ # NGKsBuildCore
11
+
12
+ NGKsBuildCore is a Python MVP build runner that executes a DAG build plan with deterministic scheduling, parallel workers, and auditable proof logs.
13
+
14
+ ## Commands
15
+
16
+ - `python -m ngksbuildcore --help`
17
+ - `python -m ngksbuildcore doctor`
18
+ - `python -m ngksbuildcore run --plan examples\hello_plan.json -j 8`
19
+ - `python -m ngksbuildcore explain --plan examples\hello_plan.json`
20
+
21
+ ## Plan Contract
22
+
23
+ Plan JSON fields:
24
+
25
+ - `base_dir` optional, defaults to plan file directory
26
+ - `nodes` array of objects with:
27
+ - `id` (string)
28
+ - `desc` (string, optional)
29
+ - `cwd` (string, optional)
30
+ - `cmd` (string or string array)
31
+ - `deps` (string array)
32
+ - `inputs` (path array)
33
+ - `outputs` (path array)
34
+ - `env` (dict, optional)
35
+
36
+ ## Proof Output
37
+
38
+ Each run writes to timestamped proof directory under `_proof` (or `NGKS_PROOF_ROOT`):
39
+
40
+ - `events.jsonl`
41
+ - `commands.jsonl`
42
+ - `summary.json`
43
+ - `summary.txt`
44
+ - `environment.txt`
45
+ - `tool_versions.txt`
46
+ - `git_status.txt`
47
+ - `git_head.txt`
48
+
49
+ ## Integration Adapters
50
+
51
+ - `ngksbuildcore.adapters.graph_adapter.run_graph_plan`
52
+ - `ngksbuildcore.adapters.devfabric_adapter.build_from_manifest`
@@ -0,0 +1,43 @@
1
+ # NGKsBuildCore
2
+
3
+ NGKsBuildCore is a Python MVP build runner that executes a DAG build plan with deterministic scheduling, parallel workers, and auditable proof logs.
4
+
5
+ ## Commands
6
+
7
+ - `python -m ngksbuildcore --help`
8
+ - `python -m ngksbuildcore doctor`
9
+ - `python -m ngksbuildcore run --plan examples\hello_plan.json -j 8`
10
+ - `python -m ngksbuildcore explain --plan examples\hello_plan.json`
11
+
12
+ ## Plan Contract
13
+
14
+ Plan JSON fields:
15
+
16
+ - `base_dir` optional, defaults to plan file directory
17
+ - `nodes` array of objects with:
18
+ - `id` (string)
19
+ - `desc` (string, optional)
20
+ - `cwd` (string, optional)
21
+ - `cmd` (string or string array)
22
+ - `deps` (string array)
23
+ - `inputs` (path array)
24
+ - `outputs` (path array)
25
+ - `env` (dict, optional)
26
+
27
+ ## Proof Output
28
+
29
+ Each run writes to timestamped proof directory under `_proof` (or `NGKS_PROOF_ROOT`):
30
+
31
+ - `events.jsonl`
32
+ - `commands.jsonl`
33
+ - `summary.json`
34
+ - `summary.txt`
35
+ - `environment.txt`
36
+ - `tool_versions.txt`
37
+ - `git_status.txt`
38
+ - `git_head.txt`
39
+
40
+ ## Integration Adapters
41
+
42
+ - `ngksbuildcore.adapters.graph_adapter.run_graph_plan`
43
+ - `ngksbuildcore.adapters.devfabric_adapter.build_from_manifest`
@@ -0,0 +1,3 @@
1
+ __all__ = ["__version__"]
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -0,0 +1,4 @@
1
+ from .devfabric_adapter import build_from_manifest
2
+ from .graph_adapter import run_graph_plan
3
+
4
+ __all__ = ["run_graph_plan", "build_from_manifest"]
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ from ..loggingx import EventLogger
9
+ from ..runner import make_proof_dir
10
+ from .graph_adapter import run_graph_plan
11
+
12
+
13
+ def build_from_manifest(manifest_path: str, jobs: int = 1, proof_dir: str | None = None) -> int:
14
+ manifest_file = Path(manifest_path).resolve()
15
+ payload = json.loads(manifest_file.read_text(encoding="utf-8"))
16
+
17
+ proof = make_proof_dir(proof_dir)
18
+ logger = EventLogger(proof)
19
+ try:
20
+ logger.emit("DEVFABRIC_MANIFEST_LOAD_OK", manifest=str(manifest_file))
21
+ plan_path = payload.get("plan_path")
22
+ if plan_path:
23
+ return run_graph_plan(plan_path=plan_path, jobs=jobs, proof_dir=proof_dir, extra_env={"NGKS_DEVFABRIC_MODE": "1"})
24
+
25
+ graph_exe = os.environ.get("NGKS_GRAPH_EXE")
26
+ if not graph_exe:
27
+ logger.emit("GRAPH_NOT_FOUND", reason="NGKS_GRAPH_EXE not set and manifest has no plan_path")
28
+ return 2
29
+
30
+ graph_args = payload.get("graph_args", [])
31
+ out_plan = payload.get("plan_out", str((manifest_file.parent / "generated_plan.json").resolve()))
32
+ cmd = [graph_exe, *graph_args, "--out", out_plan]
33
+ logger.emit("GRAPH_GENERATE_START", cmd=cmd)
34
+ proc = subprocess.run(cmd, text=True, capture_output=True, encoding="utf-8", errors="replace")
35
+ logger.emit("GRAPH_GENERATE_END", exit_code=proc.returncode, stdout=proc.stdout, stderr=proc.stderr)
36
+ if proc.returncode != 0:
37
+ return proc.returncode
38
+
39
+ return run_graph_plan(plan_path=out_plan, jobs=jobs, proof_dir=proof_dir, extra_env={"NGKS_DEVFABRIC_MODE": "1"})
40
+ finally:
41
+ logger.close()
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Mapping
4
+
5
+ from ..runner import run_build
6
+
7
+
8
+ def run_graph_plan(plan_path: str, jobs: int = 1, proof_dir: str | None = None, extra_env: Mapping[str, str] | None = None) -> int:
9
+ env = dict(extra_env or {})
10
+ return run_build(plan_path=plan_path, jobs=jobs, proof=proof_dir, extra_env=env)
@@ -0,0 +1,159 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from .plan import load_plan
11
+ from .runner import make_proof_dir, run_build
12
+
13
+
14
+ def _default_jobs(value: int | None) -> int:
15
+ if value is not None:
16
+ return value
17
+ env_value = os.environ.get("NGKS_BUILD_JOBS")
18
+ if env_value and env_value.isdigit():
19
+ return max(1, int(env_value))
20
+ return 1
21
+
22
+
23
+ def _doctor(proof: str | None) -> int:
24
+ proof_dir = make_proof_dir(proof)
25
+ checks: list[tuple[str, bool, str]] = []
26
+
27
+ py_ok = sys.version_info >= (3, 9)
28
+ checks.append(("python_version", py_ok, sys.version.split()[0]))
29
+
30
+ state_dir = Path(".ngksbuildcore")
31
+ proof_root = Path("_proof")
32
+ try:
33
+ state_dir.mkdir(parents=True, exist_ok=True)
34
+ test_file = state_dir / "_write_test.tmp"
35
+ test_file.write_text("ok\n", encoding="utf-8")
36
+ test_file.unlink()
37
+ state_ok = True
38
+ state_msg = "writable"
39
+ except Exception as exc:
40
+ state_ok = False
41
+ state_msg = str(exc)
42
+ checks.append(("state_dir_write", state_ok, state_msg))
43
+
44
+ try:
45
+ proof_root.mkdir(parents=True, exist_ok=True)
46
+ test_file = proof_root / "_write_test.tmp"
47
+ test_file.write_text("ok\n", encoding="utf-8")
48
+ test_file.unlink()
49
+ proof_ok = True
50
+ proof_msg = "writable"
51
+ except Exception as exc:
52
+ proof_ok = False
53
+ proof_msg = str(exc)
54
+ checks.append(("proof_root_write", proof_ok, proof_msg))
55
+
56
+ try:
57
+ proc = subprocess.run(
58
+ [sys.executable, "-c", "print('SPAWN_OK')"],
59
+ check=True,
60
+ capture_output=True,
61
+ text=True,
62
+ encoding="utf-8",
63
+ errors="replace",
64
+ )
65
+ spawn_ok = "SPAWN_OK" in proc.stdout
66
+ spawn_msg = proc.stdout.strip()
67
+ except Exception as exc:
68
+ spawn_ok = False
69
+ spawn_msg = str(exc)
70
+ checks.append(("process_spawn", spawn_ok, spawn_msg))
71
+
72
+ passed = all(item[1] for item in checks)
73
+ result = {
74
+ "status": "PASS" if passed else "FAIL",
75
+ "proof_dir": str(proof_dir),
76
+ "checks": [{"name": n, "ok": ok, "detail": d} for n, ok, d in checks],
77
+ }
78
+ (proof_dir / "doctor.json").write_text(json.dumps(result, indent=2, ensure_ascii=True), encoding="utf-8")
79
+ report_lines = [f"doctor: {result['status']}"] + [f"- {n}: {'PASS' if ok else 'FAIL'} ({d})" for n, ok, d in checks]
80
+ text = "\n".join(report_lines) + "\n"
81
+ (proof_dir / "doctor.txt").write_text(text, encoding="utf-8")
82
+ print(text, end="")
83
+ return 0 if passed else 1
84
+
85
+
86
+ def _explain(plan_path: str, node_id: str | None) -> int:
87
+ plan = load_plan(plan_path)
88
+ if node_id is None:
89
+ print(f"plan: {plan.plan_path}")
90
+ print(f"base_dir: {plan.base_dir}")
91
+ print(f"nodes: {len(plan.nodes)}")
92
+ for n in sorted(plan.nodes, key=lambda x: x.id):
93
+ print(f"- {n.id}: deps={n.deps} inputs={len(n.inputs)} outputs={len(n.outputs)}")
94
+ return 0
95
+
96
+ found = None
97
+ for n in plan.nodes:
98
+ if n.id == node_id:
99
+ found = n
100
+ break
101
+ if not found:
102
+ print(f"node not found: {node_id}")
103
+ return 1
104
+
105
+ print(json.dumps(
106
+ {
107
+ "id": found.id,
108
+ "desc": found.desc,
109
+ "cwd": found.cwd,
110
+ "cmd": found.cmd,
111
+ "deps": found.deps,
112
+ "inputs": found.inputs,
113
+ "outputs": found.outputs,
114
+ "env": found.env,
115
+ },
116
+ indent=2,
117
+ ensure_ascii=True,
118
+ ))
119
+ return 0
120
+
121
+
122
+ def build_parser() -> argparse.ArgumentParser:
123
+ parser = argparse.ArgumentParser(prog="ngksbuildcore", description="NGKsBuildCore MVP build runner")
124
+ sub = parser.add_subparsers(dest="command", required=True)
125
+
126
+ run_cmd = sub.add_parser("run", help="Run a build plan DAG")
127
+ run_cmd.add_argument("--plan", required=True, help="Path to plan json")
128
+ run_cmd.add_argument("--env-lock", default=None, help="Path to env_capsule.lock.json")
129
+ run_cmd.add_argument("-j", "--jobs", type=int, default=None, help="Parallel jobs")
130
+ run_cmd.add_argument("--proof", default=None, help="Proof root directory override")
131
+ run_cmd.add_argument("--pf", default=None, help="Proof root directory override (alias of --proof)")
132
+
133
+ doctor_cmd = sub.add_parser("doctor", help="Environment diagnostics")
134
+ doctor_cmd.add_argument("--proof", default=None, help="Proof root directory override")
135
+ doctor_cmd.add_argument("--pf", default=None, help="Proof root directory override (alias of --proof)")
136
+
137
+ explain_cmd = sub.add_parser("explain", help="Explain plan or node")
138
+ explain_cmd.add_argument("--plan", required=True, help="Path to plan json")
139
+ explain_cmd.add_argument("--node", default=None, help="Optional node id")
140
+
141
+ return parser
142
+
143
+
144
+ def main(argv: list[str] | None = None) -> int:
145
+ parser = build_parser()
146
+ args = parser.parse_args(argv)
147
+
148
+ if args.command == "run":
149
+ jobs = _default_jobs(args.jobs)
150
+ proof_root = args.pf or args.proof
151
+ return run_build(plan_path=args.plan, jobs=jobs, proof=proof_root, env_lock=args.env_lock)
152
+ if args.command == "doctor":
153
+ proof_root = args.pf or args.proof
154
+ return _doctor(proof_root)
155
+ if args.command == "explain":
156
+ return _explain(args.plan, args.node)
157
+
158
+ parser.print_help()
159
+ return 1
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from pathlib import Path
5
+
6
+
7
+ def normalize_path(path: str | Path, base_dir: Path) -> Path:
8
+ p = Path(path)
9
+ if not p.is_absolute():
10
+ p = (base_dir / p).resolve()
11
+ else:
12
+ p = p.resolve()
13
+ return p
14
+
15
+
16
+ def file_fingerprint(path: Path) -> str:
17
+ stat = path.stat()
18
+ raw = f"{path}:{stat.st_mtime_ns}:{stat.st_size}".encode("utf-8", errors="replace")
19
+ return hashlib.sha256(raw).hexdigest()
20
+
21
+
22
+ def input_signature(paths: list[Path]) -> str:
23
+ joined = "|".join(file_fingerprint(p) for p in sorted(paths))
24
+ return hashlib.sha256(joined.encode("utf-8", errors="replace")).hexdigest()
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import threading
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ def utc_now_iso() -> str:
11
+ return datetime.now(timezone.utc).isoformat()
12
+
13
+
14
+ class EventLogger:
15
+ def __init__(self, proof_dir: Path, console_verbose: bool = True) -> None:
16
+ self.proof_dir = proof_dir
17
+ self.proof_dir.mkdir(parents=True, exist_ok=True)
18
+ self.events_path = self.proof_dir / "events.jsonl"
19
+ self.commands_path = self.proof_dir / "commands.jsonl"
20
+ self._events_handle = self.events_path.open("a", encoding="utf-8", newline="\n")
21
+ self._commands_handle = self.commands_path.open("a", encoding="utf-8", newline="\n")
22
+ self._lock = threading.Lock()
23
+ self.console_verbose = console_verbose
24
+
25
+ def emit(self, event_type: str, **payload: Any) -> None:
26
+ evt = {"ts": utc_now_iso(), "event": event_type, **payload}
27
+ line = json.dumps(evt, ensure_ascii=True)
28
+ with self._lock:
29
+ self._events_handle.write(line + "\n")
30
+ self._events_handle.flush()
31
+
32
+ def command(self, **payload: Any) -> None:
33
+ row = {"ts": utc_now_iso(), **payload}
34
+ line = json.dumps(row, ensure_ascii=True)
35
+ with self._lock:
36
+ self._commands_handle.write(line + "\n")
37
+ self._commands_handle.flush()
38
+
39
+ def print(self, message: str) -> None:
40
+ if self.console_verbose:
41
+ print(message, flush=True)
42
+
43
+ def close(self) -> None:
44
+ with self._lock:
45
+ self._events_handle.close()
46
+ self._commands_handle.close()
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+
7
+ from .hashing import normalize_path
8
+
9
+
10
+ @dataclass(slots=True)
11
+ class PlanNode:
12
+ id: str
13
+ desc: str = ""
14
+ cwd: str | None = None
15
+ cmd: str | list[str] = ""
16
+ deps: list[str] = field(default_factory=list)
17
+ inputs: list[str] = field(default_factory=list)
18
+ outputs: list[str] = field(default_factory=list)
19
+ env: dict[str, str] = field(default_factory=dict)
20
+
21
+
22
+ @dataclass(slots=True)
23
+ class BuildPlan:
24
+ plan_path: Path
25
+ base_dir: Path
26
+ nodes: list[PlanNode]
27
+
28
+
29
+ def _as_str_list(value: object) -> list[str]:
30
+ if value is None:
31
+ return []
32
+ if not isinstance(value, list):
33
+ raise ValueError("expected a list")
34
+ return [str(x) for x in value]
35
+
36
+
37
+ def _legacy_node(raw: dict) -> PlanNode:
38
+ return PlanNode(
39
+ id=str(raw["id"]),
40
+ desc=str(raw.get("desc", "")),
41
+ cwd=raw.get("cwd"),
42
+ cmd=raw["cmd"],
43
+ deps=_as_str_list(raw.get("deps", [])),
44
+ inputs=_as_str_list(raw.get("inputs", [])),
45
+ outputs=_as_str_list(raw.get("outputs", [])),
46
+ env={str(k): str(v) for k, v in (raw.get("env") or {}).items()},
47
+ )
48
+
49
+
50
+ def _graph_action_node(raw: dict) -> PlanNode:
51
+ argv = raw.get("argv", [])
52
+ if isinstance(argv, list):
53
+ cmd: str | list[str] = [str(x) for x in argv]
54
+ else:
55
+ cmd = str(argv)
56
+ return PlanNode(
57
+ id=str(raw["id"]),
58
+ desc=str(raw.get("desc", "")),
59
+ cwd=raw.get("cwd"),
60
+ cmd=cmd,
61
+ deps=_as_str_list(raw.get("deps", [])),
62
+ inputs=_as_str_list(raw.get("inputs", [])),
63
+ outputs=_as_str_list(raw.get("outputs", [])),
64
+ env={str(k): str(v) for k, v in (raw.get("env") or {}).items()},
65
+ )
66
+
67
+
68
+ def load_plan(plan_path: str | Path) -> BuildPlan:
69
+ plan_file = Path(plan_path).resolve()
70
+ with plan_file.open("r", encoding="utf-8") as f:
71
+ payload = json.load(f)
72
+
73
+ if isinstance(payload.get("actions"), list):
74
+ raw_nodes = payload["actions"]
75
+ node_factory = _graph_action_node
76
+ elif isinstance(payload.get("nodes"), list):
77
+ raw_nodes = payload["nodes"]
78
+ node_factory = _legacy_node
79
+ else:
80
+ if isinstance(payload, dict) and isinstance(payload.get("targets"), list):
81
+ raise ValueError(
82
+ "plan json appears graph-native (targets/steps). Use `ngksgraph buildplan` for NGKsBuildCore-compatible output."
83
+ )
84
+ raise ValueError("plan json must include either an 'actions' array or a 'nodes' array")
85
+
86
+ base_dir_raw = payload.get("base_dir")
87
+ if base_dir_raw:
88
+ base_dir = normalize_path(base_dir_raw, plan_file.parent)
89
+ else:
90
+ base_dir = plan_file.parent.resolve()
91
+
92
+ nodes: list[PlanNode] = []
93
+ seen_ids: set[str] = set()
94
+ for raw in raw_nodes:
95
+ node = node_factory(raw)
96
+ if node.id in seen_ids:
97
+ raise ValueError(f"duplicate node id: {node.id}")
98
+ seen_ids.add(node.id)
99
+ nodes.append(node)
100
+
101
+ id_set = {n.id for n in nodes}
102
+ for node in nodes:
103
+ for dep in node.deps:
104
+ if dep not in id_set:
105
+ raise ValueError(f"node '{node.id}' depends on unknown node '{dep}'")
106
+
107
+ return BuildPlan(plan_path=plan_file, base_dir=base_dir, nodes=nodes)