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 CHANGED
@@ -1 +1 @@
1
- 3.76.0
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
@@ -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."
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.76.0",
3
+ "version": "3.77.0",
4
4
  "description": "The Operating System for AI Agent Teams",
5
5
  "type": "module",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arkaos-core"
3
- version = "3.76.0"
3
+ version = "3.77.0"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}