@topy-ai/maggie 0.7.3 → 0.7.5

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.
Files changed (31) hide show
  1. package/README.md +29 -9
  2. package/README.zh-TW.md +5 -1
  3. package/bin/maggie.js +18 -2
  4. package/bundled-contracts/maggie-deployment/release-runner-v1.json +14 -0
  5. package/bundled-contracts/maggiedash/README.md +22 -0
  6. package/bundled-contracts/maggiedash/agent-content-v1.json +15 -0
  7. package/bundled-contracts/maggiedash/redirect-policy-v1.json +15 -0
  8. package/bundled-contracts/maggiedash/section-identity-v1.json +14 -0
  9. package/bundled-contracts/maggiedash/telemetry-policy-v1.json +18 -0
  10. package/bundled-contracts/maggiedash/translation-cache-policy-v1.json +15 -0
  11. package/bundled-references/maggiedash-telemetry-contract.md +12 -0
  12. package/bundled-skills/maggie-dash/SKILL.md +76 -3
  13. package/bundled-skills/maggie-deployment/SKILL.md +20 -2
  14. package/bundled-skills/maggie-ops/SKILL.md +23 -2
  15. package/bundled-skills/maggie-seo-geo/SKILL.md +16 -0
  16. package/bundled-templates/maggiedash/dashboard-ui-contract.json +8 -7
  17. package/bundled-templates/maggiedash/section-registry.json +12 -0
  18. package/bundled-tools/clis/maggie_agent_content.py +75 -0
  19. package/bundled-tools/clis/maggie_dash.py +112 -1
  20. package/bundled-tools/clis/maggie_deployment.py +51 -0
  21. package/bundled-tools/clis/maggie_ops.py +23 -0
  22. package/bundled-tools/clis/maggie_verification.py +33 -0
  23. package/bundled-tools/runtime/agent_runtime.py +38 -0
  24. package/bundled-tools/runtime/analytics_traffic.py +6 -1
  25. package/bundled-tools/runtime/maggie_dash_store.py +121 -8
  26. package/bundled-tools/runtime/maggie_dash_ui.py +35 -1
  27. package/bundled-tools/runtime/maggie_sections.py +143 -0
  28. package/bundled-tools/runtime/restart_gate.py +22 -0
  29. package/bundled-tools/runtime/surface_coverage.py +48 -0
  30. package/package.json +1 -1
  31. package/references/maggiedash-telemetry-contract.md +12 -0
@@ -15,6 +15,23 @@ def _component_source(name: str, source: str) -> str:
15
15
  return match.group(0) if match else ""
16
16
 
17
17
 
18
+ def _render_body(component_source: str) -> str:
19
+ """Return the component implementation, excluding its props signature.
20
+
21
+ A declaration and its TypeScript type are evidence that a prop exists, not
22
+ evidence that the rendered UI uses it. Counting only the implementation
23
+ prevents a dead prop from passing because it appears twice in the signature.
24
+ """
25
+ # Typed destructured parameters contain their own `{...}` block. The
26
+ # function body is the last opening brace before the first return.
27
+ return_position = component_source.find("return")
28
+ opening = component_source.rfind("{", 0, return_position if return_position != -1 else len(component_source))
29
+ if opening == -1:
30
+ arrow = component_source.find("=>")
31
+ opening = component_source.find("{", arrow) if arrow != -1 else -1
32
+ return component_source[opening + 1:] if opening != -1 else ""
33
+
34
+
18
35
  def validate(contract: object, sources: Iterable[str] = ()) -> dict:
19
36
  errors: list[str] = []
20
37
  if not isinstance(contract, dict):
@@ -44,12 +61,29 @@ def validate(contract: object, sources: Iterable[str] = ()) -> dict:
44
61
  forbidden = [str(prop) for prop in component.get("forbiddenProps", [])]
45
62
  if component_source:
46
63
  signature = component_source.split("=>", 1)[0] if "=>" in component_source else component_source[:500]
64
+ body = _render_body(component_source)
47
65
  for prop in props:
48
- if len(re.findall(rf"\b{re.escape(prop)}\b", component_source)) < 2:
66
+ if len(re.findall(rf"\b{re.escape(prop)}\b", body)) < 1:
49
67
  errors.append(f"{name}.{prop} is declared but not evidenced in rendered output")
50
68
  for prop in forbidden:
51
69
  if re.search(rf"\b{re.escape(prop)}\b", signature):
52
70
  errors.append(f"{name} declares forbidden dead prop: {prop}")
71
+ dialogs = contract.get("dialogs")
72
+ if isinstance(dialogs, dict) and source:
73
+ for pattern in dialogs.get("forbiddenSourcePatterns", []):
74
+ pattern = str(pattern)
75
+ if pattern in source:
76
+ errors.append(f"dialog contract forbids source pattern: {pattern}")
77
+ required = [str(item) for item in dialogs.get("requiredLiterals", [])]
78
+ for key in ("primitive", "overlayPrimitive"):
79
+ name = str(dialogs.get(key) or "")
80
+ component_source = _component_source(name, source) if name else ""
81
+ if not component_source:
82
+ errors.append(f"dialog source is missing: {name}")
83
+ continue
84
+ for literal in required:
85
+ if literal not in component_source:
86
+ errors.append(f"{name} is missing dialog requirement: {literal}")
53
87
  return {"schemaVersion": SCHEMA, "passed": not errors, "errors": errors, "components": [item.get("name") for item in components if isinstance(item, dict)]}
54
88
 
55
89
 
@@ -0,0 +1,143 @@
1
+ """Provider-neutral section registry, copy limits, and stable section keys."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import hashlib
7
+ import json
8
+ import re
9
+ import uuid
10
+ from typing import Any, Iterable
11
+
12
+
13
+ SCHEMA = "maggiedash-section-registry.v1"
14
+ IDENTITY_SCHEMA = "maggiedash-section-identity.v1"
15
+
16
+
17
+ def validate_registry(value: object) -> dict[str, Any]:
18
+ errors: list[str] = []
19
+ if not isinstance(value, dict):
20
+ return {"schemaVersion": SCHEMA, "passed": False, "errors": ["registry must be an object"]}
21
+ if value.get("schemaVersion") != SCHEMA:
22
+ errors.append(f"schemaVersion must be {SCHEMA}")
23
+ sections = value.get("sections")
24
+ if not isinstance(sections, list) or not sections:
25
+ errors.append("sections must be a non-empty list")
26
+ sections = []
27
+ seen: set[str] = set()
28
+ for index, section in enumerate(sections):
29
+ at = f"sections[{index}]"
30
+ if not isinstance(section, dict):
31
+ errors.append(f"{at} must be an object")
32
+ continue
33
+ section_type = str(section.get("type") or "")
34
+ if not re.fullmatch(r"[a-z][a-z0-9-]*", section_type):
35
+ errors.append(f"{at}.type must be a kebab-case identifier")
36
+ if section_type in seen:
37
+ errors.append(f"duplicate section type: {section_type}")
38
+ seen.add(section_type)
39
+ for field in ("purpose", "usage", "position", "repeatable"):
40
+ if field not in section:
41
+ errors.append(f"{at}.{field} is required")
42
+ if section.get("position") not in {"opening", "body", "closing"}:
43
+ errors.append(f"{at}.position must be opening, body, or closing")
44
+ if not isinstance(section.get("repeatable"), bool):
45
+ errors.append(f"{at}.repeatable must be boolean")
46
+ fields = section.get("fields")
47
+ if not isinstance(fields, list) or not fields:
48
+ errors.append(f"{at}.fields must be a non-empty list")
49
+ continue
50
+ field_names: set[str] = set()
51
+ for field_index, field in enumerate(fields):
52
+ field_at = f"{at}.fields[{field_index}]"
53
+ if not isinstance(field, dict) or not field.get("name"):
54
+ errors.append(f"{field_at} needs a name")
55
+ continue
56
+ name = str(field["name"])
57
+ if name in field_names:
58
+ errors.append(f"duplicate field: {section_type}.{name}")
59
+ field_names.add(name)
60
+ if not isinstance(field.get("limit"), int) or field["limit"] < 0:
61
+ errors.append(f"{field_at}.limit must be a non-negative integer")
62
+ if not str(field.get("usage") or "").strip():
63
+ errors.append(f"{field_at}.usage is required")
64
+ repeats = field.get("repeats")
65
+ if repeats is not None and (not isinstance(repeats, dict) or not isinstance(repeats.get("min"), int) or not isinstance(repeats.get("max"), int) or repeats["min"] < 0 or repeats["min"] > repeats["max"]):
66
+ errors.append(f"{field_at}.repeats must declare valid min/max")
67
+ return {"schemaVersion": SCHEMA, "passed": not errors, "errors": errors, "sectionTypes": sorted(seen)}
68
+
69
+
70
+ def catalogue(registry: dict[str, Any]) -> str:
71
+ """Render a compact prompt catalogue without duplicating field guidance."""
72
+ lines: list[str] = []
73
+ for section in registry.get("sections", []):
74
+ lines.append(f"{section['type']} — {section['purpose']} | when: {section['usage']} | place: {section['position']} | {'repeatable' if section['repeatable'] else 'once'}")
75
+ for field in section.get("fields", []):
76
+ repeat = field.get("repeats")
77
+ suffix = f" [{repeat['min']}-{repeat['max']} entries]" if isinstance(repeat, dict) else ""
78
+ lines.append(f" - {field['name']} ≤{field['limit']} chars{suffix}: {field['usage']}")
79
+ return "\n".join(lines)
80
+
81
+
82
+ def copy_notes(registry: dict[str, Any], sections: object) -> dict[str, Any]:
83
+ """Return advisory copy/shape notes; over-limit copy is not rejected."""
84
+ errors: list[str] = []
85
+ notes: list[dict[str, Any]] = []
86
+ if not isinstance(sections, list):
87
+ return {"passed": False, "errors": ["sections must be a list"], "notes": []}
88
+ specs = {str(item.get("type")): item for item in registry.get("sections", []) if isinstance(item, dict)}
89
+ for index, section in enumerate(sections):
90
+ if not isinstance(section, dict):
91
+ errors.append(f"sections[{index}] must be an object")
92
+ continue
93
+ section_type = str(section.get("type") or "")
94
+ spec = specs.get(section_type)
95
+ if not spec:
96
+ errors.append(f"unknown section type: {section_type}")
97
+ continue
98
+ for field in spec.get("fields", []):
99
+ name = str(field.get("name"))
100
+ value = section.get(name)
101
+ limit = int(field.get("limit", 0))
102
+ if isinstance(value, str) and limit and len(value) > limit:
103
+ notes.append({"at": f"sections[{index}].{name}", "message": f"{len(value)} characters; layout limit is about {limit}"})
104
+ repeats = field.get("repeats")
105
+ if isinstance(repeats, dict) and isinstance(value, list) and not repeats["min"] <= len(value) <= repeats["max"]:
106
+ notes.append({"at": f"sections[{index}].{name}", "message": f"{len(value)} entries; layout expects {repeats['min']}-{repeats['max']}"})
107
+ return {"passed": not errors, "errors": errors, "notes": notes}
108
+
109
+
110
+ def new_section_id(existing: Iterable[str] = ()) -> str:
111
+ taken = set(existing)
112
+ while True:
113
+ candidate = uuid.uuid4().hex[:8]
114
+ if candidate not in taken:
115
+ return candidate
116
+
117
+
118
+ def ensure_section_ids(sections: list[dict[str, Any]], existing: Iterable[str] = ()) -> list[dict[str, Any]]:
119
+ result = copy.deepcopy(sections)
120
+ existing_ids = set(existing)
121
+ taken = set(existing_ids)
122
+ for section in result:
123
+ current = str(section.get("id") or "")
124
+ if not current or current in existing_ids or current in taken:
125
+ section["id"] = new_section_id(taken)
126
+ taken.add(str(section["id"]))
127
+ return result
128
+
129
+
130
+ def section_key(section: object, index: int) -> str:
131
+ if isinstance(section, dict) and isinstance(section.get("id"), str) and section["id"]:
132
+ return section["id"]
133
+ return str(index)
134
+
135
+
136
+ def translation_key(page_id: str, section: object, index: int, field: str) -> str:
137
+ return f"page.{page_id}.sections.{section_key(section, index)}.{field}"
138
+
139
+
140
+ def section_id_migration(page_id: str, sections: list[dict[str, Any]]) -> dict[str, Any]:
141
+ identified = ensure_section_ids(sections)
142
+ mappings = [{"from": f"page.{page_id}.sections.{index}.", "to": f"page.{page_id}.sections.{section['id']}."} for index, section in enumerate(identified)]
143
+ return {"schemaVersion": IDENTITY_SCHEMA, "pageId": page_id, "sections": identified, "renameOrder": sorted(mappings, key=lambda item: len(item["from"]), reverse=True), "sourceChecksum": hashlib.sha256(json.dumps(sections, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode()).hexdigest()}
@@ -0,0 +1,22 @@
1
+ """Evidence gate for writes performed outside the running application."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def validate(value: object) -> dict[str, Any]:
9
+ errors: list[str] = []
10
+ if not isinstance(value, dict):
11
+ return {"schemaVersion": "maggie-restart-gate.v1", "passed": False, "errors": ["restart gate must be an object"]}
12
+ if value.get("schemaVersion") != "maggie-restart-gate.v1":
13
+ errors.append("unsupported restart gate schema")
14
+ if value.get("outOfBandWrite") is not True:
15
+ errors.append("outOfBandWrite must be true for this gate")
16
+ if value.get("restartRequired") is not True:
17
+ errors.append("restartRequired must be true after an out-of-band write")
18
+ if not str(value.get("restartCommand") or "").strip():
19
+ errors.append("restartCommand is required")
20
+ if value.get("verificationAfterRestart") is not True:
21
+ errors.append("verificationAfterRestart must be true")
22
+ return {"schemaVersion": "maggie-restart-gate.v1", "passed": not errors, "errors": errors, "order": ["write", "restart", "verify"]}
@@ -0,0 +1,48 @@
1
+ """Coverage checks that follow the surface and locale changed by a release."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ SCHEMA = "maggie-surface-coverage.v1"
9
+
10
+
11
+ def validate_contract(contract: object) -> dict[str, Any]:
12
+ errors: list[str] = []
13
+ if not isinstance(contract, dict):
14
+ return {"schemaVersion": SCHEMA, "passed": False, "errors": ["coverage contract must be an object"]}
15
+ if contract.get("schemaVersion") != SCHEMA:
16
+ errors.append(f"schemaVersion must be {SCHEMA}")
17
+ surfaces = contract.get("surfaces")
18
+ if not isinstance(surfaces, list) or not surfaces:
19
+ errors.append("surfaces must be a non-empty list")
20
+ surfaces = []
21
+ seen: set[tuple[str, str]] = set()
22
+ for index, item in enumerate(surfaces):
23
+ if not isinstance(item, dict) or not item.get("name") or not isinstance(item.get("routes"), list) or not item["routes"] or not isinstance(item.get("locales"), list) or not item["locales"]:
24
+ errors.append(f"surfaces[{index}] needs name, routes, and locales")
25
+ continue
26
+ for locale in item["locales"]:
27
+ key = (str(item["name"]), str(locale))
28
+ if key in seen:
29
+ errors.append(f"duplicate surface/locale: {key[0]}/{key[1]}")
30
+ seen.add(key)
31
+ return {"schemaVersion": SCHEMA, "passed": not errors, "errors": errors, "expectedPairs": sorted(f"{surface}/{locale}" for surface, locale in seen)}
32
+
33
+
34
+ def compare(contract: dict[str, Any], observations: object) -> dict[str, Any]:
35
+ checked = validate_contract(contract)
36
+ if not checked["passed"]:
37
+ return {**checked, "observedPairs": [], "missing": [], "failed": []}
38
+ expected = {(str(item["name"]), str(locale)) for item in contract["surfaces"] for locale in item["locales"]}
39
+ observed: dict[tuple[str, str], dict[str, Any]] = {}
40
+ for item in observations if isinstance(observations, list) else []:
41
+ if not isinstance(item, dict):
42
+ continue
43
+ key = (str(item.get("surface") or ""), str(item.get("locale") or ""))
44
+ if key in expected:
45
+ observed[key] = item
46
+ missing = sorted(f"{surface}/{locale}" for surface, locale in expected - observed.keys())
47
+ failed = sorted(f"{surface}/{locale}" for (surface, locale), item in observed.items() if item.get("passed") is not True or int(item.get("status", 200)) >= 400)
48
+ return {"schemaVersion": SCHEMA, "passed": not missing and not failed, "expectedPairs": sorted(f"{surface}/{locale}" for surface, locale in expected), "observedPairs": sorted(f"{surface}/{locale}" for surface, locale in observed), "missing": missing, "failed": failed}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topy-ai/maggie",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "description": "Install and manage Maggie Skills for AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,12 @@
1
+ # MaggieDash telemetry contract
2
+
3
+ Every measurement path must call the shared first-party traffic filter before
4
+ incrementing a counter or writing a measurement event. This includes page
5
+ views, 404/not-found rows, redirect hits, booking clicks, analytics events and
6
+ operational audit counters. Filtering only page views leaves synthetic sweeps
7
+ in the other totals.
8
+
9
+ Use `should_record_measurement(event)` from
10
+ `tools/runtime/analytics_traffic.py`. The filter accepts explicit Maggie/test
11
+ markers and known toolchain user agents; it never guesses identity from IP
12
+ addresses or private user fields.