arkaos 3.76.0 → 3.77.0
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/VERSION +1 -1
- package/arka/skills/flow/SKILL.md +14 -0
- package/config/constitution.yaml +5 -0
- package/core/governance/__pycache__/activation_tracker.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/design_system_lint.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/design_system_lint_cli.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/dna_fidelity.cpython-313.pyc +0 -0
- package/core/governance/design_system_lint.py +197 -0
- package/core/governance/design_system_lint_cli.py +92 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.77.0
|
|
@@ -81,6 +81,20 @@ spec why divergence is justified. Manual audit:
|
|
|
81
81
|
Dispatch specialists via the `Agent` tool. The squad lead from Phase 3
|
|
82
82
|
names them. Specialists run in parallel when work is independent.
|
|
83
83
|
|
|
84
|
+
**Design-system check on UI work (PR6 v3.77.0, SHOULD `design-system-locked`).**
|
|
85
|
+
Before dispatching frontend/landing specialists, run the per-project
|
|
86
|
+
linter to surface UI/UX drift:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
python -m core.governance.design_system_lint_cli <project_path>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
If the project has a `design-system.yaml`, violations come back with
|
|
93
|
+
file:line + the suggestion text declared in the YAML. The frontend
|
|
94
|
+
specialist should fix existing violations OR document why the new work
|
|
95
|
+
diverges. Projects without a `design-system.yaml` skip the check.
|
|
96
|
+
Template: `docs/examples/design-system-example.yaml`.
|
|
97
|
+
|
|
84
98
|
**Experience injection (PR3 v3.74.0).** When a specialist is dispatched,
|
|
85
99
|
Synapse layer `L2.6 AgentExperiences`
|
|
86
100
|
(`core/synapse/agent_experiences_layer.py`) detects the
|
package/config/constitution.yaml
CHANGED
|
@@ -217,6 +217,11 @@ enforcement_levels:
|
|
|
217
217
|
rule: "Bottom-line first output. Lead with answer, then why, then how. Confidence tags on assessments."
|
|
218
218
|
enforcement: "See config/standards/communication.md for full standard"
|
|
219
219
|
|
|
220
|
+
# ─── Rule added in PR6 Squad Intelligence Upgrade (2026-05-28) ───────
|
|
221
|
+
- id: design-system-locked
|
|
222
|
+
rule: "Each project SHOULD declare a `design-system.yaml` at its root listing tokens (colors, spacing, fonts), allowed_components, file_globs to scan, and forbidden_patterns with suggestions. The per-project linter (core.governance.design_system_lint) detects UI/UX drift — hex literals outside the palette, inline style attributes, raw HTML where a Nuxt UI component exists, etc. Adoption is opt-in per project (no YAML at project root → no violations). v3.77.0 is advisory-only; pre-commit hook integration lands in v3.77.x once the rule sets stabilise across the operator's projects."
|
|
223
|
+
enforcement: "Run via `python -m core.governance.design_system_lint_cli <project_path>` (text or JSON output, optional --exit-on-violations). Example template at `docs/examples/design-system-example.yaml`."
|
|
224
|
+
|
|
220
225
|
# ─── Rule added in PR5 Squad Intelligence Upgrade (2026-05-28) ───────
|
|
221
226
|
- id: dna-fidelity-warn
|
|
222
227
|
rule: "Agent outputs are compared against the `signature_markers` block in each agent's YAML at the end of every turn. Forbidden patterns (avoid_patterns) and missing opening phrases (opening_phrases) generate FidelityViolation records in ~/.arkaos/telemetry/dna-fidelity.jsonl. v3.76.0 is soft-warn — violations are recorded for telemetry and operator review, not blocked. Hard-block mode lands later once the marker set is calibrated against real production usage."
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Design System Linter — PR6 Squad Intelligence Upgrade v3.77.0.
|
|
2
|
+
|
|
3
|
+
Scans a project for forbidden patterns declared in its design-system.yaml.
|
|
4
|
+
Opt-in per project: no YAML at root means no violations. Advisory-only in
|
|
5
|
+
v3.77.0; pre-commit hook integration lands in v3.77.x.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import asdict, dataclass, field # noqa: F401 (asdict kept for callers)
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Iterator
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ─── Dataclasses ──────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class DesignSystem:
|
|
22
|
+
"""Loaded design-system.yaml for a project."""
|
|
23
|
+
|
|
24
|
+
version: int = 1
|
|
25
|
+
project: str = ""
|
|
26
|
+
tokens: dict = field(default_factory=dict)
|
|
27
|
+
allowed_components: list[str] = field(default_factory=list)
|
|
28
|
+
file_globs: list[str] = field(default_factory=list)
|
|
29
|
+
forbidden_patterns: list[dict] = field(default_factory=list)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class DesignViolation:
|
|
34
|
+
file: str # relative to project_path, forward slashes
|
|
35
|
+
line: int # 1-indexed
|
|
36
|
+
pattern: str # the regex string from forbidden_patterns
|
|
37
|
+
suggestion: str # suggestion text from forbidden_patterns
|
|
38
|
+
matched_text: str # the actual matched substring (cap 200 chars)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
def _default_file_globs() -> list[str]:
|
|
44
|
+
return ["**/*.vue", "**/*.tsx", "**/*.jsx"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _escape_glob_literal(s: str) -> str:
|
|
48
|
+
"""Escape a literal glob segment (no ** inside)."""
|
|
49
|
+
return re.escape(s).replace(r"\*", "[^/]*").replace(r"\?", "[^/]")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _glob_to_regex(glob_pattern: str) -> re.Pattern[str]:
|
|
53
|
+
"""Translate a glob pattern (with ** support) to a compiled regex.
|
|
54
|
+
|
|
55
|
+
Gitignore convention: ``**/`` = zero-or-more ``dir/`` prefixes (including
|
|
56
|
+
zero), so ``**/*.vue`` matches both ``App.vue`` and ``src/App.vue``.
|
|
57
|
+
Bare ``*`` = any non-slash run; ``?`` = one non-slash char.
|
|
58
|
+
"""
|
|
59
|
+
# Split on ** tokens, keeping them as delimiters.
|
|
60
|
+
tokens = re.split(r"(\*\*)", glob_pattern)
|
|
61
|
+
result = ""
|
|
62
|
+
for i, tok in enumerate(tokens):
|
|
63
|
+
if tok != "**":
|
|
64
|
+
result += _escape_glob_literal(tok)
|
|
65
|
+
continue
|
|
66
|
+
nxt = tokens[i + 1] if i + 1 < len(tokens) else ""
|
|
67
|
+
if nxt.startswith("/"):
|
|
68
|
+
# **/ → consume the slash, emit zero-or-more dir/ groups.
|
|
69
|
+
tokens[i + 1] = nxt[1:]
|
|
70
|
+
result += "(?:[^/]+/)*"
|
|
71
|
+
else:
|
|
72
|
+
# trailing ** or ** not followed by / → match any remaining path.
|
|
73
|
+
result += ".*"
|
|
74
|
+
return re.compile(f"^{result}$")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _glob_match(pattern: str, rel_path: str) -> bool:
|
|
78
|
+
"""Return True when rel_path (forward slashes) matches the glob pattern."""
|
|
79
|
+
try:
|
|
80
|
+
return bool(_glob_to_regex(pattern).match(rel_path))
|
|
81
|
+
except re.error:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _iter_matching_files(
|
|
86
|
+
project_path: Path, file_globs: list[str]
|
|
87
|
+
) -> Iterator[Path]:
|
|
88
|
+
"""Yield all files under project_path matching any glob in file_globs."""
|
|
89
|
+
seen: set[Path] = set()
|
|
90
|
+
for glob in file_globs:
|
|
91
|
+
for path in project_path.rglob("*"):
|
|
92
|
+
if not path.is_file():
|
|
93
|
+
continue
|
|
94
|
+
rel = path.relative_to(project_path).as_posix()
|
|
95
|
+
if _glob_match(glob, rel) and path not in seen:
|
|
96
|
+
seen.add(path)
|
|
97
|
+
yield path
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _is_excluded(rel_path: str, exclude_paths: list[str]) -> bool:
|
|
101
|
+
"""Return True if rel_path matches any exclude_paths glob or is design-system.yaml."""
|
|
102
|
+
if rel_path == "design-system.yaml":
|
|
103
|
+
return True
|
|
104
|
+
return any(_glob_match(exc, rel_path) for exc in exclude_paths)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _scan_file_for_pattern(
|
|
108
|
+
file_path: Path,
|
|
109
|
+
project_path: Path,
|
|
110
|
+
compiled_re: re.Pattern[str],
|
|
111
|
+
pattern: str,
|
|
112
|
+
suggestion: str,
|
|
113
|
+
) -> Iterator[DesignViolation]:
|
|
114
|
+
"""Read one file and yield a DesignViolation for every regex match."""
|
|
115
|
+
rel = file_path.relative_to(project_path).as_posix()
|
|
116
|
+
try:
|
|
117
|
+
text = file_path.read_text(encoding="utf-8", errors="replace")
|
|
118
|
+
except OSError:
|
|
119
|
+
return
|
|
120
|
+
for lineno, line in enumerate(text.splitlines(), start=1):
|
|
121
|
+
for match in compiled_re.finditer(line):
|
|
122
|
+
yield DesignViolation(
|
|
123
|
+
file=rel,
|
|
124
|
+
line=lineno,
|
|
125
|
+
pattern=pattern,
|
|
126
|
+
suggestion=suggestion,
|
|
127
|
+
matched_text=match.group(0)[:200],
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ─── Public API ───────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
def load_design_system(project_path: Path) -> DesignSystem | None:
|
|
134
|
+
"""Load design-system.yaml from project_path root.
|
|
135
|
+
|
|
136
|
+
Returns None when the file is absent or malformed.
|
|
137
|
+
"""
|
|
138
|
+
yaml_path = project_path / "design-system.yaml"
|
|
139
|
+
if not yaml_path.is_file():
|
|
140
|
+
return None
|
|
141
|
+
try:
|
|
142
|
+
raw = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
|
|
143
|
+
except (OSError, yaml.YAMLError):
|
|
144
|
+
return None
|
|
145
|
+
if not isinstance(raw, dict):
|
|
146
|
+
return None
|
|
147
|
+
return DesignSystem(
|
|
148
|
+
version=int(raw.get("version") or 1),
|
|
149
|
+
project=str(raw.get("project") or ""),
|
|
150
|
+
tokens=dict(raw.get("tokens") or {}),
|
|
151
|
+
allowed_components=list(raw.get("allowed_components") or []),
|
|
152
|
+
file_globs=list(raw.get("file_globs") or []),
|
|
153
|
+
forbidden_patterns=list(raw.get("forbidden_patterns") or []),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def lint_project(project_path: Path) -> list[DesignViolation]:
|
|
158
|
+
"""Scan project_path for design-system violations.
|
|
159
|
+
|
|
160
|
+
Returns an empty list when the project path does not exist, has no
|
|
161
|
+
design-system.yaml, or the YAML is malformed.
|
|
162
|
+
"""
|
|
163
|
+
if not project_path.is_dir():
|
|
164
|
+
return []
|
|
165
|
+
ds = load_design_system(project_path)
|
|
166
|
+
if ds is None:
|
|
167
|
+
return []
|
|
168
|
+
globs = ds.file_globs if ds.file_globs else _default_file_globs()
|
|
169
|
+
violations: list[DesignViolation] = []
|
|
170
|
+
for fp_dict in ds.forbidden_patterns:
|
|
171
|
+
if not isinstance(fp_dict, dict):
|
|
172
|
+
continue
|
|
173
|
+
_collect_pattern_violations(project_path, globs, fp_dict, violations)
|
|
174
|
+
return violations
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _collect_pattern_violations(
|
|
178
|
+
project_path: Path,
|
|
179
|
+
file_globs: list[str],
|
|
180
|
+
fp_dict: dict,
|
|
181
|
+
violations: list[DesignViolation],
|
|
182
|
+
) -> None:
|
|
183
|
+
"""Compile one forbidden pattern and append matching violations in-place."""
|
|
184
|
+
raw_pattern = fp_dict.get("pattern", "")
|
|
185
|
+
suggestion = fp_dict.get("suggestion", "")
|
|
186
|
+
exclude_paths: list[str] = list(fp_dict.get("exclude_paths") or [])
|
|
187
|
+
try:
|
|
188
|
+
compiled = re.compile(raw_pattern)
|
|
189
|
+
except re.error:
|
|
190
|
+
return
|
|
191
|
+
for file_path in _iter_matching_files(project_path, file_globs):
|
|
192
|
+
rel = file_path.relative_to(project_path).as_posix()
|
|
193
|
+
if _is_excluded(rel, exclude_paths):
|
|
194
|
+
continue
|
|
195
|
+
violations.extend(
|
|
196
|
+
_scan_file_for_pattern(file_path, project_path, compiled, raw_pattern, suggestion)
|
|
197
|
+
)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""CLI for the Design System Linter (PR6 Squad Intelligence Upgrade v3.77.0).
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python -m core.governance.design_system_lint_cli <project_path> [--format text|json] [--exit-on-violations]
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
python -m core.governance.design_system_lint_cli /path/to/project
|
|
8
|
+
python -m core.governance.design_system_lint_cli /path/to/project --format json --exit-on-violations
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from collections import defaultdict
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from core.governance.design_system_lint import (
|
|
20
|
+
DesignViolation,
|
|
21
|
+
lint_project,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
26
|
+
parser = argparse.ArgumentParser(
|
|
27
|
+
prog="python -m core.governance.design_system_lint_cli",
|
|
28
|
+
description="Scan a project for design-system violations.",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument("project_path", help="Path to the project root containing design-system.yaml.")
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--format",
|
|
33
|
+
choices=["text", "json"],
|
|
34
|
+
default="text",
|
|
35
|
+
help="Output format (default: text).",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--exit-on-violations",
|
|
39
|
+
action="store_true",
|
|
40
|
+
help="Exit with code 1 when violations are found.",
|
|
41
|
+
)
|
|
42
|
+
return parser
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _print_text(violations: list[DesignViolation]) -> None:
|
|
46
|
+
"""Group violations by file, sorted by file then line, and pretty-print."""
|
|
47
|
+
print(f"{len(violations)} design-system violation(s) found:")
|
|
48
|
+
by_file: dict[str, list[DesignViolation]] = defaultdict(list)
|
|
49
|
+
for v in violations:
|
|
50
|
+
by_file[v.file].append(v)
|
|
51
|
+
for file in sorted(by_file):
|
|
52
|
+
for v in sorted(by_file[file], key=lambda x: x.line):
|
|
53
|
+
truncated = v.matched_text[:60] + ("..." if len(v.matched_text) > 60 else "")
|
|
54
|
+
print(f" {v.file}:{v.line} {truncated}")
|
|
55
|
+
print(f" → {v.suggestion}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _print_json(violations: list[DesignViolation]) -> None:
|
|
59
|
+
"""Emit one JSON line per violation followed by a summary line (jsonl)."""
|
|
60
|
+
for v in violations:
|
|
61
|
+
print(json.dumps({
|
|
62
|
+
"file": v.file,
|
|
63
|
+
"line": v.line,
|
|
64
|
+
"pattern": v.pattern,
|
|
65
|
+
"suggestion": v.suggestion,
|
|
66
|
+
"matched_text": v.matched_text,
|
|
67
|
+
}))
|
|
68
|
+
print(json.dumps({"summary": True, "count": len(violations)}))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def main(argv: list[str] | None = None) -> int:
|
|
72
|
+
parser = _build_parser()
|
|
73
|
+
args = parser.parse_args(argv if argv is not None else sys.argv[1:])
|
|
74
|
+
violations = lint_project(Path(args.project_path))
|
|
75
|
+
|
|
76
|
+
if not violations:
|
|
77
|
+
if args.format == "json":
|
|
78
|
+
print(json.dumps({"violations": [], "count": 0}))
|
|
79
|
+
else:
|
|
80
|
+
print("No design-system violations.")
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
if args.format == "json":
|
|
84
|
+
_print_json(violations)
|
|
85
|
+
else:
|
|
86
|
+
_print_text(violations)
|
|
87
|
+
|
|
88
|
+
return 1 if args.exit_on_violations else 0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__": # pragma: no cover
|
|
92
|
+
sys.exit(main())
|
package/package.json
CHANGED