@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.
- package/README.md +34 -6
- package/README.zh-TW.md +5 -1
- package/bin/maggie.js +18 -2
- package/bundled-contracts/maggie-deployment/release-runner-v1.json +14 -0
- package/bundled-contracts/maggiedash/README.md +6 -0
- package/bundled-contracts/maggiedash/agent-content-v1.json +17 -0
- package/bundled-contracts/maggiedash/section-identity-v1.json +14 -0
- package/bundled-contracts/maggiedash/translation-cache-policy-v1.json +15 -0
- package/bundled-references/localization-extraction-render-prd.md +55 -0
- package/bundled-skills/maggie-content-localization/SKILL.md +1 -1
- package/bundled-skills/maggie-dash/SKILL.md +53 -1
- package/bundled-skills/maggie-deployment/SKILL.md +20 -2
- package/bundled-skills/maggie-feedback/SKILL.md +4 -1
- package/bundled-skills/maggie-ops/SKILL.md +17 -0
- package/bundled-skills/maggie-seo-geo/SKILL.md +20 -0
- package/bundled-templates/maggiedash/section-registry.json +12 -0
- package/bundled-tools/clis/maggie_agent_content.py +135 -0
- package/bundled-tools/clis/maggie_dash.py +44 -0
- package/bundled-tools/clis/maggie_deployment.py +51 -0
- package/bundled-tools/clis/maggie_feedback.py +35 -7
- package/bundled-tools/clis/maggie_ops.py +23 -0
- package/bundled-tools/clis/maggie_verification.py +33 -0
- package/bundled-tools/runtime/agent_runtime.py +38 -0
- package/bundled-tools/runtime/maggie_dash_store.py +51 -0
- package/bundled-tools/runtime/maggie_sections.py +143 -0
- package/bundled-tools/runtime/restart_gate.py +22 -0
- package/bundled-tools/runtime/surface_coverage.py +122 -0
- package/package.json +1 -1
- 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
|
@@ -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.
|