@topy-ai/maggie 0.7.4 → 0.7.6

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 (29) hide show
  1. package/README.md +34 -6
  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 +6 -0
  6. package/bundled-contracts/maggiedash/agent-content-v1.json +17 -0
  7. package/bundled-contracts/maggiedash/section-identity-v1.json +14 -0
  8. package/bundled-contracts/maggiedash/translation-cache-policy-v1.json +15 -0
  9. package/bundled-references/localization-extraction-render-prd.md +55 -0
  10. package/bundled-skills/maggie-content-localization/SKILL.md +1 -1
  11. package/bundled-skills/maggie-dash/SKILL.md +53 -1
  12. package/bundled-skills/maggie-deployment/SKILL.md +20 -2
  13. package/bundled-skills/maggie-feedback/SKILL.md +4 -1
  14. package/bundled-skills/maggie-ops/SKILL.md +17 -0
  15. package/bundled-skills/maggie-seo-geo/SKILL.md +20 -0
  16. package/bundled-templates/maggiedash/section-registry.json +12 -0
  17. package/bundled-tools/clis/maggie_agent_content.py +135 -0
  18. package/bundled-tools/clis/maggie_dash.py +44 -0
  19. package/bundled-tools/clis/maggie_deployment.py +51 -0
  20. package/bundled-tools/clis/maggie_feedback.py +35 -7
  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/maggie_dash_store.py +51 -0
  25. package/bundled-tools/runtime/maggie_sections.py +143 -0
  26. package/bundled-tools/runtime/restart_gate.py +22 -0
  27. package/bundled-tools/runtime/surface_coverage.py +122 -0
  28. package/package.json +1 -1
  29. package/references/localization-extraction-render-prd.md +55 -0
@@ -0,0 +1,122 @@
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
+ REQUIRED_EVIDENCE = ("status", "canonical", "indexability", "metadata")
10
+
11
+
12
+ def validate_contract(contract: object) -> dict[str, Any]:
13
+ errors: list[str] = []
14
+ if not isinstance(contract, dict):
15
+ return {"schemaVersion": SCHEMA, "passed": False, "errors": ["coverage contract must be an object"]}
16
+ if contract.get("schemaVersion") != SCHEMA:
17
+ errors.append(f"schemaVersion must be {SCHEMA}")
18
+ surfaces = contract.get("surfaces")
19
+ if not isinstance(surfaces, list) or not surfaces:
20
+ errors.append("surfaces must be a non-empty list")
21
+ surfaces = []
22
+ seen: set[tuple[str, str]] = set()
23
+ for index, item in enumerate(surfaces):
24
+ 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"]:
25
+ errors.append(f"surfaces[{index}] needs name, routes, and locales")
26
+ continue
27
+ required = item.get("requiredEvidence")
28
+ if not isinstance(required, list) or not required or any(value not in REQUIRED_EVIDENCE for value in required):
29
+ errors.append(f"surfaces[{index}] needs valid requiredEvidence")
30
+ metadata = item.get("requiredMetadata")
31
+ if not isinstance(metadata, list) or any(not isinstance(value, str) or not value for value in metadata):
32
+ errors.append(f"surfaces[{index}] needs requiredMetadata")
33
+ route_values = [str(route) for route in item["routes"]]
34
+ if len(route_values) != len(set(route_values)):
35
+ errors.append(f"duplicate route in surface: {item['name']}")
36
+ locale_values = [str(locale) for locale in item["locales"]]
37
+ if len(locale_values) != len(set(locale_values)):
38
+ errors.append(f"duplicate locale in surface: {item['name']}")
39
+ for locale in item["locales"]:
40
+ key = (str(item["name"]), str(locale))
41
+ if key in seen:
42
+ errors.append(f"duplicate surface/locale: {key[0]}/{key[1]}")
43
+ seen.add(key)
44
+ expected = [f"{item['name']}/{locale}/{route}" for item in surfaces if isinstance(item, dict) and item.get("name") for locale in item.get("locales", []) for route in item.get("routes", [])]
45
+ return {"schemaVersion": SCHEMA, "passed": not errors, "errors": errors, "expectedPairs": sorted(f"{surface}/{locale}" for surface, locale in seen), "expectedRoutes": sorted(expected)}
46
+
47
+
48
+ def _truthy_check(value: object) -> bool:
49
+ if value is True:
50
+ return True
51
+ return isinstance(value, dict) and value.get("ok") is True
52
+
53
+
54
+ def _key_text(key: tuple[str, str, str]) -> str:
55
+ return f"{key[0]}/{key[1]}|{key[2]}"
56
+
57
+
58
+ def _route_result(item: dict[str, Any], required: list[str], required_metadata: list[str]) -> tuple[bool, list[str]]:
59
+ missing: list[str] = []
60
+ checks = item.get("checks") if isinstance(item.get("checks"), dict) else {}
61
+ if "status" in required:
62
+ try:
63
+ if int(item.get("status")) >= 400:
64
+ missing.append("status")
65
+ except (TypeError, ValueError):
66
+ missing.append("status")
67
+ if "canonical" in required and not _truthy_check(item.get("canonical", checks.get("canonical"))):
68
+ missing.append("canonical")
69
+ indexability = item.get("indexable", item.get("indexability", checks.get("indexability")))
70
+ if "indexability" in required and not isinstance(indexability, bool):
71
+ missing.append("indexability")
72
+ if "metadata" in required:
73
+ metadata = item.get("metadata") if isinstance(item.get("metadata"), dict) else checks.get("metadata")
74
+ if not isinstance(metadata, dict) or any(metadata.get(field) is not True for field in required_metadata):
75
+ missing.append("metadata")
76
+ if item.get("passed") is not True:
77
+ missing.append("passed")
78
+ return not missing, sorted(set(missing))
79
+
80
+
81
+ def compare(contract: dict[str, Any], observations: object) -> dict[str, Any]:
82
+ checked = validate_contract(contract)
83
+ if not checked["passed"]:
84
+ return {**checked, "observedPairs": [], "missing": [], "failed": []}
85
+ expected = {(str(item["name"]), str(locale), str(route)) for item in contract["surfaces"] for locale in item["locales"] for route in item["routes"]}
86
+ observed: dict[tuple[str, str, str], dict[str, Any]] = {}
87
+ duplicate_keys: list[str] = []
88
+ observation_count = 0
89
+ route_results: list[dict[str, Any]] = []
90
+ for item in observations if isinstance(observations, list) else []:
91
+ if not isinstance(item, dict):
92
+ continue
93
+ observation_count += 1
94
+ key = (str(item.get("surface") or ""), str(item.get("locale") or ""), str(item.get("route") or ""))
95
+ if key in expected:
96
+ if key in observed:
97
+ duplicate_keys.append(_key_text(key))
98
+ else:
99
+ observed[key] = item
100
+ missing = sorted(_key_text(key) for key in expected - observed.keys())
101
+ failed: list[str] = []
102
+ for key, item in observed.items():
103
+ surface = next(value for value in contract["surfaces"] if str(value["name"]) == key[0])
104
+ passed, missing_evidence = _route_result(item, list(surface["requiredEvidence"]), list(surface["requiredMetadata"]))
105
+ key_text = _key_text(key)
106
+ route_results.append({"route": key_text, "passed": passed, "missingEvidence": missing_evidence})
107
+ if not passed:
108
+ failed.append(key_text)
109
+ failed = sorted(set(failed))
110
+ return {
111
+ "schemaVersion": SCHEMA,
112
+ "passed": not missing and not failed and not duplicate_keys,
113
+ "expectedPairs": sorted(f"{surface}/{locale}" for surface, locale, _route in expected),
114
+ "expectedRoutes": sorted(_key_text(key) for key in expected),
115
+ "observedPairs": sorted(f"{surface}/{locale}" for surface, locale, _route in observed),
116
+ "observedRoutes": sorted(_key_text(key) for key in observed),
117
+ "observationCount": observation_count,
118
+ "duplicates": sorted(set(duplicate_keys)),
119
+ "missing": missing,
120
+ "failed": failed,
121
+ "routeResults": sorted(route_results, key=lambda value: value["route"]),
122
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topy-ai/maggie",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "description": "Install and manage Maggie Skills for AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,55 @@
1
+ # Localization source extraction and rendered validation
2
+
3
+ ## Problem
4
+
5
+ `maggie-content-localization` currently accepts a prepared `content.json` and
6
+ validates the resulting job. It does not extract translatable strings from a
7
+ real project, so malformed JSX fragments, multiline text nodes, untranslated
8
+ Latin terms, and script mismatches can pass the record validator while still
9
+ breaking the rendered page.
10
+
11
+ ## Proposed stable workflow
12
+
13
+ Add a read-only extractor and a separate rendered validation gate:
14
+
15
+ ```bash
16
+ maggie localization extract --project . --source-dir src \
17
+ --routes-file docs/routes.tsv --output .maggie/localization/source.json
18
+ maggie localization plan --content .maggie/localization/source.json ...
19
+ maggie localization validate .maggie/localization/<job>.json \
20
+ --source .maggie/localization/source.json \
21
+ --render-report .maggie/localization/rendered.json
22
+ ```
23
+
24
+ The extractor must use an AST-aware adapter for JSX/TSX/Astro/Vue/Svelte
25
+ where available, with a conservative HTML fallback. It must never rewrite
26
+ source files. Each extracted string records file, line, route, source hash,
27
+ attribute/text-node kind, and an explicit translatable/non-translatable
28
+ decision. Fragments containing braces or JavaScript operators are rejected or
29
+ reported for review, not interpreted as text.
30
+
31
+ The validation report must cover multiline nodes, required metadata, protected
32
+ facts, script expectations, glossary/allowlist exceptions, and browser output.
33
+ The rendered report must identify route, screenshot, console errors, failed
34
+ asset requests, placeholder count, and untranslated required strings. Any
35
+ failure keeps the locale non-indexable and blocks publish.
36
+
37
+ ## Acceptance criteria
38
+
39
+ - JSX conditionals and expressions are never emitted as translation strings.
40
+ - Text nodes spanning multiple lines are extracted and tested.
41
+ - Script/Latin-word checks support all registered languages and configurable
42
+ proper-noun/glossary allowlists.
43
+ - Source and translated strings are linked by stable IDs and source revision.
44
+ - A clean extraction plus clean rendered report is required before approval;
45
+ record-only validation is not sufficient.
46
+ - Unit fixtures cover Astro/JSX/Vue/Svelte/HTML, false positives, protected
47
+ facts, multiline nodes, script checks, and route-level browser errors.
48
+ - The feature remains local-first, dependency-adaptable, redacted, and
49
+ backward-compatible with existing `content.json` input.
50
+
51
+ ## Out of scope
52
+
53
+ Automatic translation quality scoring, silent source rewriting, and automatic
54
+ publish. Translation generation and owner/Admin approval remain separate
55
+ operations.