@grifhinz/logics-manager 2.2.0 → 2.3.1
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.
- package/README.md +95 -1
- package/VERSION +1 -1
- package/clients/README.md +9 -0
- package/clients/shared-web/media/css/board.css +658 -0
- package/clients/shared-web/media/css/details.css +457 -0
- package/clients/shared-web/media/css/layout.css +123 -0
- package/clients/shared-web/media/css/toolbar.css +576 -0
- package/clients/shared-web/media/harnessApi.js +324 -0
- package/clients/shared-web/media/hostApi.js +213 -0
- package/clients/shared-web/media/hostApiContract.js +55 -0
- package/clients/shared-web/media/icon.png +0 -0
- package/clients/shared-web/media/layoutController.js +246 -0
- package/clients/shared-web/media/logics.svg +7 -0
- package/clients/shared-web/media/logicsModel.js +910 -0
- package/clients/shared-web/media/main.css +112 -0
- package/clients/shared-web/media/main.js +3 -0
- package/clients/shared-web/media/mainApp.js +1005 -0
- package/clients/shared-web/media/mainCore.js +604 -0
- package/clients/shared-web/media/mainInteractionHandlers.js +324 -0
- package/clients/shared-web/media/mainInteractions.js +378 -0
- package/clients/shared-web/media/renderBoard.js +3 -0
- package/clients/shared-web/media/renderBoardApp.js +1339 -0
- package/clients/shared-web/media/renderDetails.js +685 -0
- package/clients/shared-web/media/renderMarkdown.js +449 -0
- package/clients/shared-web/media/toolsPanelLayout.js +172 -0
- package/clients/shared-web/media/uiStatus.js +54 -0
- package/clients/shared-web/media/webviewChrome.js +405 -0
- package/clients/shared-web/media/webviewPersistence.js +116 -0
- package/clients/shared-web/media/webviewSelectors.js +491 -0
- package/clients/viewer/README.md +5 -0
- package/clients/viewer/browser-host.js +847 -0
- package/clients/viewer/index.html +237 -0
- package/clients/viewer/viewer.css +433 -0
- package/logics_manager/assist.py +9 -142
- package/logics_manager/assist_handoff.py +132 -0
- package/logics_manager/assist_surface.py +38 -0
- package/logics_manager/cli.py +78 -5
- package/logics_manager/flow.py +126 -24
- package/logics_manager/flow_evidence.py +63 -0
- package/logics_manager/update_check.py +138 -0
- package/logics_manager/viewer.py +533 -0
- package/package.json +12 -6
- package/pyproject.toml +1 -1
package/logics_manager/assist.py
CHANGED
|
@@ -11,6 +11,8 @@ import subprocess
|
|
|
11
11
|
from shutil import which
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
+
from .assist_handoff import build_handoff as _build_handoff
|
|
15
|
+
from .assist_surface import build_changed_surface_summary as _build_changed_surface_summary
|
|
14
16
|
from .config import ConfigError, find_repo_root, load_repo_config
|
|
15
17
|
from .doctor import doctor_payload
|
|
16
18
|
from .lint import lint_payload
|
|
@@ -643,35 +645,6 @@ def _git_changed_paths(repo_root: Path) -> list[str]:
|
|
|
643
645
|
return [line.strip() for line in completed.stdout.splitlines() if line.strip()]
|
|
644
646
|
|
|
645
647
|
|
|
646
|
-
def _git_lines(repo_root: Path, args: list[str]) -> list[str]:
|
|
647
|
-
try:
|
|
648
|
-
completed = subprocess.run(
|
|
649
|
-
["git", *args],
|
|
650
|
-
cwd=repo_root,
|
|
651
|
-
stdout=subprocess.PIPE,
|
|
652
|
-
stderr=subprocess.DEVNULL,
|
|
653
|
-
text=True,
|
|
654
|
-
check=False,
|
|
655
|
-
)
|
|
656
|
-
except OSError:
|
|
657
|
-
return []
|
|
658
|
-
if completed.returncode != 0:
|
|
659
|
-
return []
|
|
660
|
-
return [line.strip() for line in completed.stdout.splitlines() if line.strip()]
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
def _git_range_changed_paths(repo_root: Path, since: str) -> list[str]:
|
|
664
|
-
return sorted(set(_git_lines(repo_root, ["diff", "--name-only", "--relative=.", f"{since}..HEAD"])))
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
def _git_range_commits(repo_root: Path, since: str) -> list[dict[str, str]]:
|
|
668
|
-
commits: list[dict[str, str]] = []
|
|
669
|
-
for line in _git_lines(repo_root, ["log", "--oneline", f"{since}..HEAD"]):
|
|
670
|
-
commit, _, subject = line.partition(" ")
|
|
671
|
-
commits.append({"commit": commit, "subject": subject})
|
|
672
|
-
return commits
|
|
673
|
-
|
|
674
|
-
|
|
675
648
|
def _is_low_risk_generated_path(path: str) -> bool:
|
|
676
649
|
normalized = path.strip().replace("\\", "/")
|
|
677
650
|
filename = normalized.rsplit("/", 1)[-1]
|
|
@@ -754,7 +727,7 @@ def _render_diff_risk_text(payload: dict[str, object]) -> str:
|
|
|
754
727
|
def _summarize_commit_scope(changed_paths: list[str]) -> tuple[str, str]:
|
|
755
728
|
if not changed_paths:
|
|
756
729
|
return "root", "No changes detected; nothing to commit."
|
|
757
|
-
if any(path.startswith("src/") for path in changed_paths):
|
|
730
|
+
if any(path.startswith("clients/vscode/src/") or path.startswith("clients/shared-web/media/") for path in changed_paths):
|
|
758
731
|
return "plugin", "Plugin surface changes detected."
|
|
759
732
|
if any(path.startswith("logics_manager/") for path in changed_paths):
|
|
760
733
|
return "python-runtime", "Native Logics manager changes detected."
|
|
@@ -796,55 +769,20 @@ def _build_commit_plan(changed_paths: list[str]) -> dict[str, object]:
|
|
|
796
769
|
}
|
|
797
770
|
|
|
798
771
|
|
|
799
|
-
def _build_changed_surface_summary(changed_paths: list[str]) -> dict[str, object]:
|
|
800
|
-
category_counter: Counter[str] = Counter()
|
|
801
|
-
for path in changed_paths:
|
|
802
|
-
normalized = path.replace("\\", "/")
|
|
803
|
-
if normalized.startswith("src/"):
|
|
804
|
-
category_counter["plugin"] += 1
|
|
805
|
-
elif normalized.startswith("logics_manager/"):
|
|
806
|
-
category_counter["python-runtime"] += 1
|
|
807
|
-
elif normalized.startswith("logics/"):
|
|
808
|
-
category_counter["workflow-docs"] += 1
|
|
809
|
-
elif normalized.startswith("tests/") or "/tests/" in normalized or normalized.startswith("python_tests/"):
|
|
810
|
-
category_counter["tests"] += 1
|
|
811
|
-
elif normalized.endswith(".md"):
|
|
812
|
-
category_counter["docs"] += 1
|
|
813
|
-
else:
|
|
814
|
-
category_counter["other"] += 1
|
|
815
|
-
primary = category_counter.most_common(1)[0][0] if category_counter else "clean"
|
|
816
|
-
summary = {
|
|
817
|
-
"clean": "No changed surface was detected.",
|
|
818
|
-
"plugin": "The plugin surface is the dominant change area.",
|
|
819
|
-
"python-runtime": "The native Python runtime is the dominant change area.",
|
|
820
|
-
"workflow-docs": "Workflow documentation is the dominant change area.",
|
|
821
|
-
"tests": "Tests are the dominant change area.",
|
|
822
|
-
"docs": "Markdown documentation is the dominant change area.",
|
|
823
|
-
"other": "Mixed repository changes are present.",
|
|
824
|
-
}.get(primary, "Mixed repository changes are present.")
|
|
825
|
-
return {
|
|
826
|
-
"summary": summary,
|
|
827
|
-
"primary_category": primary,
|
|
828
|
-
"counts": dict(sorted(category_counter.items())),
|
|
829
|
-
"changed_paths": changed_paths,
|
|
830
|
-
"review_recommended": primary not in {"clean", "docs"} and bool(changed_paths),
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
|
|
834
772
|
def _build_validation_checklist(changed_paths: list[str]) -> dict[str, object]:
|
|
835
773
|
surface = _build_changed_surface_summary(changed_paths)
|
|
836
774
|
checks: list[str] = [
|
|
837
|
-
"Run `python3 -m pytest
|
|
775
|
+
"Run `python3 -m pytest tests/python/test_logics_manager_cli.py -q`.",
|
|
838
776
|
"Run `python3 -m compileall logics_manager`.",
|
|
839
777
|
"Run `npm run lint:logics`.",
|
|
840
778
|
]
|
|
841
|
-
if any(path.startswith("src/") for path in changed_paths):
|
|
779
|
+
if any(path.startswith("clients/vscode/src/") or path.startswith("clients/shared-web/media/") for path in changed_paths):
|
|
842
780
|
checks.append("Run the plugin test suite that exercises the VS Code entrypoints.")
|
|
843
781
|
if any(path.startswith("logics_manager/") for path in changed_paths):
|
|
844
782
|
checks.append("Smoke-test `python3 -m logics_manager --help` and the affected native subcommands.")
|
|
845
783
|
if any(path.startswith("logics/") for path in changed_paths):
|
|
846
784
|
checks.append("Run `python3 -m logics_manager lint --require-status` and inspect the workflow docs manually.")
|
|
847
|
-
if any(path.startswith("tests/") or path.startswith("
|
|
785
|
+
if any(path.startswith("tests/") or path.startswith("tests/python/") for path in changed_paths):
|
|
848
786
|
checks.append("Run the focused affected tests before broad regression sweeps.")
|
|
849
787
|
if not changed_paths:
|
|
850
788
|
checks.append("No validation needed beyond a clean smoke check; there are no tracked changes.")
|
|
@@ -860,15 +798,15 @@ def _build_test_impact_summary(changed_paths: list[str]) -> dict[str, object]:
|
|
|
860
798
|
categories = _build_changed_surface_summary(changed_paths)["counts"]
|
|
861
799
|
recommended: list[str] = []
|
|
862
800
|
if "python-runtime" in categories:
|
|
863
|
-
recommended.append("python3 -m pytest
|
|
801
|
+
recommended.append("python3 -m pytest tests/python/test_logics_manager_cli.py -q")
|
|
864
802
|
if "plugin" in categories:
|
|
865
803
|
recommended.append("npm run lint")
|
|
866
804
|
if "workflow-docs" in categories:
|
|
867
805
|
recommended.append("npm run lint:logics")
|
|
868
806
|
if "tests" in categories:
|
|
869
|
-
recommended.append("python3 -m pytest
|
|
807
|
+
recommended.append("python3 -m pytest tests/python/test_logics_manager_cli.py -q")
|
|
870
808
|
if not recommended:
|
|
871
|
-
recommended.append("python3 -m pytest
|
|
809
|
+
recommended.append("python3 -m pytest tests/python/test_logics_manager_cli.py -q")
|
|
872
810
|
return {
|
|
873
811
|
"summary": "Recommended test order derived from the current change surface.",
|
|
874
812
|
"categories": categories,
|
|
@@ -2079,77 +2017,6 @@ def _build_closure_summary(repo_root: Path, ref: str | None) -> dict[str, object
|
|
|
2079
2017
|
}
|
|
2080
2018
|
|
|
2081
2019
|
|
|
2082
|
-
def _doc_title_from_path(path: Path) -> str:
|
|
2083
|
-
try:
|
|
2084
|
-
lines = path.read_text(encoding="utf-8").splitlines()
|
|
2085
|
-
except OSError:
|
|
2086
|
-
return path.stem
|
|
2087
|
-
for line in lines:
|
|
2088
|
-
if line.startswith("## "):
|
|
2089
|
-
payload = line.removeprefix("## ").strip()
|
|
2090
|
-
if " - " in payload:
|
|
2091
|
-
return payload.split(" - ", 1)[1].strip()
|
|
2092
|
-
return payload
|
|
2093
|
-
return path.stem
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
def _validation_lines_from_task(path: Path) -> list[str]:
|
|
2097
|
-
try:
|
|
2098
|
-
lines = path.read_text(encoding="utf-8").splitlines()
|
|
2099
|
-
except OSError:
|
|
2100
|
-
return []
|
|
2101
|
-
values: list[str] = []
|
|
2102
|
-
for line in _section_lines(lines, "Validation"):
|
|
2103
|
-
stripped = line.strip()
|
|
2104
|
-
if not stripped.startswith("- "):
|
|
2105
|
-
continue
|
|
2106
|
-
value = stripped[2:].strip()
|
|
2107
|
-
if value and not value.lower().startswith("run `") and not value.lower().startswith("run the "):
|
|
2108
|
-
values.append(value)
|
|
2109
|
-
return values
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
def _build_handoff(repo_root: Path, since: str) -> dict[str, object]:
|
|
2113
|
-
changed_paths = _git_range_changed_paths(repo_root, since)
|
|
2114
|
-
commits = _git_range_commits(repo_root, since)
|
|
2115
|
-
surface = _build_changed_surface_summary(changed_paths)
|
|
2116
|
-
logics_docs: list[dict[str, object]] = []
|
|
2117
|
-
validations: list[str] = []
|
|
2118
|
-
for rel_path in changed_paths:
|
|
2119
|
-
if not rel_path.startswith("logics/") or not rel_path.endswith(".md"):
|
|
2120
|
-
continue
|
|
2121
|
-
path = repo_root / rel_path
|
|
2122
|
-
kind = path.parent.name
|
|
2123
|
-
entry = {
|
|
2124
|
-
"path": rel_path,
|
|
2125
|
-
"ref": path.stem,
|
|
2126
|
-
"kind": kind,
|
|
2127
|
-
"title": _doc_title_from_path(path),
|
|
2128
|
-
"status": _doc_status(path) if path.is_file() else "Unknown",
|
|
2129
|
-
}
|
|
2130
|
-
logics_docs.append(entry)
|
|
2131
|
-
if kind == "tasks":
|
|
2132
|
-
validations.extend(_validation_lines_from_task(path))
|
|
2133
|
-
next_actions = [
|
|
2134
|
-
"Run lint/audit if not already included in validation evidence.",
|
|
2135
|
-
"Review changed files before committing or handing off.",
|
|
2136
|
-
]
|
|
2137
|
-
if any(path.startswith("logics_manager/") for path in changed_paths):
|
|
2138
|
-
next_actions.append("Run `PYTHONPATH=\"$PWD\" pytest python_tests -q` for Python CLI changes.")
|
|
2139
|
-
if any(path.startswith("src/") for path in changed_paths):
|
|
2140
|
-
next_actions.append("Run the TypeScript/vitest checks for extension changes.")
|
|
2141
|
-
return {
|
|
2142
|
-
"since": since,
|
|
2143
|
-
"commit_count": len(commits),
|
|
2144
|
-
"commits": commits,
|
|
2145
|
-
"changed_paths": changed_paths,
|
|
2146
|
-
"surface": surface,
|
|
2147
|
-
"logics_docs": logics_docs,
|
|
2148
|
-
"validations": sorted(set(validations)),
|
|
2149
|
-
"next_actions": next_actions,
|
|
2150
|
-
}
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
2020
|
def _build_context_pack(repo_root: Path, seed_ref: str, *, mode: str, profile: str) -> dict[str, object]:
|
|
2154
2021
|
docs = _workflow_docs(repo_root)
|
|
2155
2022
|
selected: list[Path] = []
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
from .assist_surface import build_changed_surface_summary
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _git_lines(repo_root: Path, args: list[str]) -> list[str]:
|
|
10
|
+
try:
|
|
11
|
+
completed = subprocess.run(
|
|
12
|
+
["git", *args],
|
|
13
|
+
cwd=repo_root,
|
|
14
|
+
stdout=subprocess.PIPE,
|
|
15
|
+
stderr=subprocess.DEVNULL,
|
|
16
|
+
text=True,
|
|
17
|
+
check=False,
|
|
18
|
+
)
|
|
19
|
+
except OSError:
|
|
20
|
+
return []
|
|
21
|
+
if completed.returncode != 0:
|
|
22
|
+
return []
|
|
23
|
+
return [line.strip() for line in completed.stdout.splitlines() if line.strip()]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _git_range_changed_paths(repo_root: Path, since: str) -> list[str]:
|
|
27
|
+
return sorted(set(_git_lines(repo_root, ["diff", "--name-only", f"{since}..HEAD"])))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _git_range_commits(repo_root: Path, since: str) -> list[dict[str, str]]:
|
|
31
|
+
commits: list[dict[str, str]] = []
|
|
32
|
+
for line in _git_lines(repo_root, ["log", "--oneline", f"{since}..HEAD"]):
|
|
33
|
+
commit, _, subject = line.partition(" ")
|
|
34
|
+
commits.append({"commit": commit, "subject": subject})
|
|
35
|
+
return commits
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _section_lines(lines: list[str], heading: str) -> list[str]:
|
|
39
|
+
start_idx = None
|
|
40
|
+
target = heading.strip().lower()
|
|
41
|
+
for idx, line in enumerate(lines):
|
|
42
|
+
if line.startswith("# ") and line[2:].strip().lower() == target:
|
|
43
|
+
start_idx = idx + 1
|
|
44
|
+
break
|
|
45
|
+
if start_idx is None:
|
|
46
|
+
return []
|
|
47
|
+
out: list[str] = []
|
|
48
|
+
for idx in range(start_idx, len(lines)):
|
|
49
|
+
line = lines[idx]
|
|
50
|
+
if line.startswith("# "):
|
|
51
|
+
break
|
|
52
|
+
out.append(line)
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _doc_status(path: Path) -> str:
|
|
57
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
58
|
+
stripped = line.strip()
|
|
59
|
+
if stripped.startswith("> Status:"):
|
|
60
|
+
return stripped.split(":", 1)[1].strip()
|
|
61
|
+
return "Unknown"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _doc_title_from_path(path: Path) -> str:
|
|
65
|
+
try:
|
|
66
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
67
|
+
except OSError:
|
|
68
|
+
return path.stem
|
|
69
|
+
for line in lines:
|
|
70
|
+
if line.startswith("## "):
|
|
71
|
+
payload = line.removeprefix("## ").strip()
|
|
72
|
+
if " - " in payload:
|
|
73
|
+
return payload.split(" - ", 1)[1].strip()
|
|
74
|
+
return payload
|
|
75
|
+
return path.stem
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _validation_lines_from_task(path: Path) -> list[str]:
|
|
79
|
+
try:
|
|
80
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
81
|
+
except OSError:
|
|
82
|
+
return []
|
|
83
|
+
values: list[str] = []
|
|
84
|
+
for line in _section_lines(lines, "Validation"):
|
|
85
|
+
stripped = line.strip()
|
|
86
|
+
if not stripped.startswith("- "):
|
|
87
|
+
continue
|
|
88
|
+
value = stripped[2:].strip()
|
|
89
|
+
if value and not value.lower().startswith("run `") and not value.lower().startswith("run the "):
|
|
90
|
+
values.append(value)
|
|
91
|
+
return values
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def build_handoff(repo_root: Path, since: str) -> dict[str, object]:
|
|
95
|
+
changed_paths = _git_range_changed_paths(repo_root, since)
|
|
96
|
+
commits = _git_range_commits(repo_root, since)
|
|
97
|
+
surface = build_changed_surface_summary(changed_paths)
|
|
98
|
+
logics_docs: list[dict[str, object]] = []
|
|
99
|
+
validations: list[str] = []
|
|
100
|
+
for rel_path in changed_paths:
|
|
101
|
+
if not rel_path.startswith("logics/") or not rel_path.endswith(".md"):
|
|
102
|
+
continue
|
|
103
|
+
path = repo_root / rel_path
|
|
104
|
+
kind = path.parent.name
|
|
105
|
+
entry = {
|
|
106
|
+
"path": rel_path,
|
|
107
|
+
"ref": path.stem,
|
|
108
|
+
"kind": kind,
|
|
109
|
+
"title": _doc_title_from_path(path),
|
|
110
|
+
"status": _doc_status(path) if path.is_file() else "Unknown",
|
|
111
|
+
}
|
|
112
|
+
logics_docs.append(entry)
|
|
113
|
+
if kind == "tasks":
|
|
114
|
+
validations.extend(_validation_lines_from_task(path))
|
|
115
|
+
next_actions = [
|
|
116
|
+
"Run lint/audit if not already included in validation evidence.",
|
|
117
|
+
"Review changed files before committing or handing off.",
|
|
118
|
+
]
|
|
119
|
+
if any(path.startswith("logics_manager/") for path in changed_paths):
|
|
120
|
+
next_actions.append("Run `PYTHONPATH=\"$PWD\" pytest tests/python -q` for Python CLI changes.")
|
|
121
|
+
if any(path.startswith("clients/vscode/src/") or path.startswith("clients/shared-web/media/") for path in changed_paths):
|
|
122
|
+
next_actions.append("Run the TypeScript/vitest checks for extension changes.")
|
|
123
|
+
return {
|
|
124
|
+
"since": since,
|
|
125
|
+
"commit_count": len(commits),
|
|
126
|
+
"commits": commits,
|
|
127
|
+
"changed_paths": changed_paths,
|
|
128
|
+
"surface": surface,
|
|
129
|
+
"logics_docs": logics_docs,
|
|
130
|
+
"validations": sorted(set(validations)),
|
|
131
|
+
"next_actions": next_actions,
|
|
132
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_changed_surface_summary(changed_paths: list[str]) -> dict[str, object]:
|
|
7
|
+
category_counter: Counter[str] = Counter()
|
|
8
|
+
for path in changed_paths:
|
|
9
|
+
normalized = path.replace("\\", "/")
|
|
10
|
+
if normalized.startswith("clients/vscode/src/") or normalized.startswith("clients/shared-web/media/"):
|
|
11
|
+
category_counter["plugin"] += 1
|
|
12
|
+
elif normalized.startswith("logics_manager/"):
|
|
13
|
+
category_counter["python-runtime"] += 1
|
|
14
|
+
elif normalized.startswith("logics/"):
|
|
15
|
+
category_counter["workflow-docs"] += 1
|
|
16
|
+
elif normalized.startswith("tests/") or "/tests/" in normalized or normalized.startswith("tests/python/"):
|
|
17
|
+
category_counter["tests"] += 1
|
|
18
|
+
elif normalized.endswith(".md"):
|
|
19
|
+
category_counter["docs"] += 1
|
|
20
|
+
else:
|
|
21
|
+
category_counter["other"] += 1
|
|
22
|
+
primary = category_counter.most_common(1)[0][0] if category_counter else "clean"
|
|
23
|
+
summary = {
|
|
24
|
+
"clean": "No changed surface was detected.",
|
|
25
|
+
"plugin": "The plugin surface is the dominant change area.",
|
|
26
|
+
"python-runtime": "The native Python runtime is the dominant change area.",
|
|
27
|
+
"workflow-docs": "Workflow documentation is the dominant change area.",
|
|
28
|
+
"tests": "Tests are the dominant change area.",
|
|
29
|
+
"docs": "Markdown documentation is the dominant change area.",
|
|
30
|
+
"other": "Mixed repository changes are present.",
|
|
31
|
+
}.get(primary, "Mixed repository changes are present.")
|
|
32
|
+
return {
|
|
33
|
+
"summary": summary,
|
|
34
|
+
"primary_category": primary,
|
|
35
|
+
"counts": dict(sorted(category_counter.items())),
|
|
36
|
+
"changed_paths": changed_paths,
|
|
37
|
+
"review_recommended": primary not in {"clean", "docs"} and bool(changed_paths),
|
|
38
|
+
}
|
package/logics_manager/cli.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import json
|
|
4
5
|
from importlib import metadata
|
|
5
6
|
import subprocess
|
|
6
7
|
import sys
|
|
8
|
+
import sysconfig
|
|
7
9
|
from shutil import which
|
|
8
10
|
from pathlib import Path
|
|
9
11
|
|
|
@@ -20,6 +22,7 @@ from .lint import lint_payload, render_lint
|
|
|
20
22
|
from .sync import search_logics_docs_payload
|
|
21
23
|
from .doctor import render_doctor
|
|
22
24
|
from .termstyle import colorize_help
|
|
25
|
+
from .update_check import get_update_notice
|
|
23
26
|
|
|
24
27
|
|
|
25
28
|
DEFAULT_SELF_UPDATE_PY_PACKAGE = "logics-manager"
|
|
@@ -37,6 +40,7 @@ ROOT_COMMANDS = (
|
|
|
37
40
|
"product-consistency",
|
|
38
41
|
"status",
|
|
39
42
|
"lint",
|
|
43
|
+
"view",
|
|
40
44
|
"config",
|
|
41
45
|
"doctor",
|
|
42
46
|
"mcp",
|
|
@@ -88,6 +92,7 @@ def _build_root_help() -> str:
|
|
|
88
92
|
" product-consistency Check product brief lineage links.",
|
|
89
93
|
" status Summarize open workflow docs and next actions.",
|
|
90
94
|
" search Search workflow docs directly.",
|
|
95
|
+
" view Start a local read-only browser viewer for the Logics corpus.",
|
|
91
96
|
"",
|
|
92
97
|
"Validation:",
|
|
93
98
|
" lint Check filenames, headings, indicators, and changed-doc hygiene.",
|
|
@@ -130,6 +135,60 @@ def get_cli_version() -> str:
|
|
|
130
135
|
return "0.0.0"
|
|
131
136
|
|
|
132
137
|
|
|
138
|
+
def _is_running_inside_venv() -> bool:
|
|
139
|
+
return sys.prefix != getattr(sys, "base_prefix", sys.prefix)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _is_externally_managed_python() -> bool:
|
|
143
|
+
if _is_running_inside_venv():
|
|
144
|
+
return False
|
|
145
|
+
stdlib = sysconfig.get_path("stdlib")
|
|
146
|
+
return bool(stdlib and (Path(stdlib) / "EXTERNALLY-MANAGED").exists())
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _is_running_from_npm_package() -> bool:
|
|
150
|
+
package_json = Path(__file__).resolve().parents[1] / "package.json"
|
|
151
|
+
try:
|
|
152
|
+
payload = json.loads(package_json.read_text(encoding="utf-8"))
|
|
153
|
+
except (OSError, json.JSONDecodeError):
|
|
154
|
+
return False
|
|
155
|
+
return payload.get("name") == DEFAULT_SELF_UPDATE_PACKAGE
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _print_externally_managed_update_guidance(package_name: str) -> None:
|
|
159
|
+
print(
|
|
160
|
+
"\n".join(
|
|
161
|
+
[
|
|
162
|
+
"This Python installation is externally managed, so pip cannot safely update logics-manager in the system environment.",
|
|
163
|
+
"",
|
|
164
|
+
"Recommended fix:",
|
|
165
|
+
" sudo apt update",
|
|
166
|
+
" sudo apt install pipx python3-venv",
|
|
167
|
+
" pipx ensurepath",
|
|
168
|
+
f" pipx install --force {package_name}",
|
|
169
|
+
"",
|
|
170
|
+
"If you installed the npm package instead, run:",
|
|
171
|
+
f" npm install -g {DEFAULT_SELF_UPDATE_PACKAGE}@latest",
|
|
172
|
+
"",
|
|
173
|
+
"Advanced override, at your own risk:",
|
|
174
|
+
f" logics-manager self-update --manager pip --break-system-packages",
|
|
175
|
+
]
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _is_json_mode(argv: list[str]) -> bool:
|
|
181
|
+
return "--json" in argv or any(argv[index] == "--format" and index + 1 < len(argv) and argv[index + 1] == "json" for index in range(len(argv)))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _maybe_print_update_notice(command: str, argv: list[str]) -> None:
|
|
185
|
+
if command in {"self-update", "mcp", "view"} or _is_json_mode(argv) or not sys.stdout.isatty():
|
|
186
|
+
return
|
|
187
|
+
notice = get_update_notice(get_cli_version())
|
|
188
|
+
if notice:
|
|
189
|
+
print(notice, file=sys.stderr)
|
|
190
|
+
|
|
191
|
+
|
|
133
192
|
def main(argv: list[str] | None = None) -> int:
|
|
134
193
|
if argv is None:
|
|
135
194
|
argv = sys.argv[1:]
|
|
@@ -147,6 +206,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
147
206
|
command = argv[0]
|
|
148
207
|
if command not in ROOT_COMMANDS:
|
|
149
208
|
raise SystemExit(f"Unsupported command: {command}")
|
|
209
|
+
_maybe_print_update_notice(command, argv)
|
|
150
210
|
|
|
151
211
|
rest = argv[1:]
|
|
152
212
|
if command == "config":
|
|
@@ -192,20 +252,29 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
192
252
|
parser.add_argument("--manager", choices=("auto", "pip", "npm"), default="auto")
|
|
193
253
|
parser.add_argument("--package", default=DEFAULT_SELF_UPDATE_PACKAGE)
|
|
194
254
|
parser.add_argument("--python-package", default=DEFAULT_SELF_UPDATE_PY_PACKAGE)
|
|
255
|
+
parser.add_argument("--break-system-packages", action="store_true")
|
|
195
256
|
parser.add_argument("--dry-run", action="store_true")
|
|
196
257
|
parsed = parser.parse_args(rest)
|
|
197
258
|
|
|
198
259
|
manager = parsed.manager
|
|
199
260
|
if manager == "auto":
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
except metadata.PackageNotFoundError:
|
|
203
|
-
manager = "npm" if which("npm") else "pip"
|
|
261
|
+
if _is_running_from_npm_package() and which("npm"):
|
|
262
|
+
manager = "npm"
|
|
204
263
|
else:
|
|
205
|
-
|
|
264
|
+
try:
|
|
265
|
+
metadata.version(parsed.python_package)
|
|
266
|
+
except metadata.PackageNotFoundError:
|
|
267
|
+
manager = "npm" if which("npm") else "pip"
|
|
268
|
+
else:
|
|
269
|
+
manager = "pip"
|
|
206
270
|
|
|
207
271
|
if manager == "pip":
|
|
272
|
+
if _is_externally_managed_python() and not parsed.break_system_packages:
|
|
273
|
+
_print_externally_managed_update_guidance(parsed.python_package)
|
|
274
|
+
return 1
|
|
208
275
|
command = [sys.executable, "-m", "pip", "install", "--upgrade", parsed.python_package]
|
|
276
|
+
if parsed.break_system_packages:
|
|
277
|
+
command.append("--break-system-packages")
|
|
209
278
|
else:
|
|
210
279
|
npm = which("npm")
|
|
211
280
|
if not npm:
|
|
@@ -240,6 +309,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
240
309
|
from .mcp import main as mcp_main
|
|
241
310
|
|
|
242
311
|
return mcp_main(rest)
|
|
312
|
+
if command == "view":
|
|
313
|
+
from .viewer import main as viewer_main
|
|
314
|
+
|
|
315
|
+
return viewer_main(rest)
|
|
243
316
|
if command == "audit":
|
|
244
317
|
audit_parser = build_audit_parser()
|
|
245
318
|
parsed = audit_parser.parse_args(rest)
|