@topy-ai/maggie 0.7.19 → 0.7.21

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 CHANGED
@@ -192,6 +192,7 @@ maggie seo social-cards ... # per-page og:image format/dimension audit
192
192
  maggie seo head-tags ... # rendered-shell head metadata drift audit
193
193
  maggie ops favicon-check ... # served favicon behaviour check
194
194
  maggie deployment | migration | release | analytics | schedule
195
+ maggie deployment readiness --project PATH
195
196
  maggie migration identity --identity-file FILE [--expected-file FILE]
196
197
  maggie deployment canary --asset URL=SHA256 --render-report report.json
197
198
  maggie design icon-inventory --source-dir src --runtime assets/icons.css
@@ -280,6 +281,25 @@ The canary report requires a screenshot and zero console errors, missing
280
281
  assets, and visual placeholders for every route. It records safe cache headers
281
282
  only and never stores response bodies, cookies, or credentials.
282
283
 
284
+ Unit regression does not establish runtime release readiness. Produce unit
285
+ evidence and then validate all five release evidence slots:
286
+
287
+ ```bash
288
+ python3 tools/tests/run_regression.py \
289
+ --report .maggie/verification/unit-regression.json
290
+ maggie deployment readiness --project . \
291
+ --package-report .maggie/verification/package-smoke.json \
292
+ --browser-report .maggie/verification/browser-evidence.json \
293
+ --rendered-canary .maggie/deployment-canary.json \
294
+ --deployment-preflight .maggie/release-preflight.json \
295
+ --output .maggie/deployment-readiness.json
296
+ ```
297
+
298
+ The readiness report follows `maggie-deployment-readiness.v1`. It reports unit
299
+ regression, package smoke, host browser evidence, rendered canary, and
300
+ deployment preflight separately. Missing host adapter evidence is
301
+ `inconclusive`; only five passing slots produce `passed`.
302
+
283
303
  ### Localization and analytics release gates
284
304
 
285
305
  Extract real page strings before creating a localization job, then require a
@@ -305,8 +325,8 @@ artifact schemas.
305
325
  Recommended upgrade sequence for the current release:
306
326
 
307
327
  ```bash
308
- npx @topy-ai/maggie@0.7.19 update --project . --force
309
- npx @topy-ai/maggie@0.7.19 cleanup --project .
328
+ npx @topy-ai/maggie@0.7.21 update --project . --force
329
+ npx @topy-ai/maggie@0.7.21 cleanup --project .
310
330
  ```
311
331
 
312
332
  Maintainers should pass npm credentials through the repository helper, never
@@ -316,7 +336,11 @@ as a command-line argument:
316
336
  node scripts/publish-npm.mjs --maggie-env-file ../.env
317
337
  ```
318
338
 
319
- The 0.7.19 workflow adds documentation hygiene audits, live URL API contract
339
+ The 0.7.21 workflow adds operation-specific localization quality checks,
340
+ explicit deployment readiness evidence states, and serialized package assembly.
341
+ The 0.7.20 workflow completes the MaggieDash Activity and Navigation
342
+ workspaces and dashboard UI v3 contract. The 0.7.19 workflow adds
343
+ documentation hygiene audits, live URL API contract
320
344
  validation, host-supplied schema reader audits, dashboard runtime evidence,
321
345
  and provider-neutral activity-log and managed-navigation contracts. The 0.7.18
322
346
  workflow adds staged/untracked changed-surface detection, scenario
package/README.zh-TW.md CHANGED
@@ -8,7 +8,7 @@ Codex、Claude Code 與相容的 coding agents。
8
8
  ## 安裝
9
9
 
10
10
  ```bash
11
- npx @topy-ai/maggie@0.7.19 init --agent all
11
+ npx @topy-ai/maggie@0.7.21 init --agent all
12
12
  npx @topy-ai/maggie doctor --project .
13
13
  ```
14
14
 
@@ -25,7 +25,10 @@ maggie doctor --project . --require-bootstrap --strict
25
25
  deployment、memory、feedback 和 MaggieDash。內容先 draft/review,外部寫入、
26
26
  publish 與 production deployment 需要明確確認。
27
27
 
28
- 0.7.19 加入文件 hygiene audit、live URL API contract、schema reader audit、
28
+ 0.7.21 增加 localization polish/rewrite 的 operation-specific quality checks、
29
+ deployment readiness evidence 狀態,以及並發 package assembly 保護。
30
+ 0.7.20 完成 MaggieDash Activity 與 Navigation workspace,以及 dashboard UI v3
31
+ contract。0.7.19 加入文件 hygiene audit、live URL API contract、schema reader audit、
29
32
  dashboard runtime evidence,以及 activity-log 和 managed-navigation contracts。
30
33
  0.7.18 加入 staged/untracked changed-surface release gate、QA run 與 release
31
34
  整合、market/locale-aware service variant route、provider capability matrix
@@ -37,6 +40,10 @@ MaggieDash 也提供穩定 section identity、可重用 section arrangement、
37
40
  section registry、短期 agent content bridge,以及 translation out-of-band write
38
41
  後的 restart gate。`maggie doctor` 會比較 install manifest 與磁碟上的實際 skills。
39
42
 
43
+ `maggie deployment readiness` 會分開檢查 unit regression、package smoke、host
44
+ browser evidence、rendered canary 與 deployment preflight。缺少必要 adapter 時
45
+ 回傳 `inconclusive`,只有五組 evidence 全部通過才回傳 `passed`。
46
+
40
47
  新版 release gates:
41
48
 
42
49
  ```bash
package/bin/maggie.js CHANGED
@@ -108,6 +108,7 @@ Usage:
108
108
  maggie service convert-page <page-path> --project PATH
109
109
  maggie service match-pages --project PATH --pages-dir src/pages
110
110
  maggie deployment --project PATH --target vps-with-cloudflare-dns
111
+ maggie deployment readiness --project PATH
111
112
  maggie deployment canary --project PATH --asset URL=SHA256 --render-report report.json --output docs/deployment-canary.json
112
113
  maggie migration --project PATH --environment staging
113
114
  maggie migration identity --identity-file FILE [--expected-file FILE]
@@ -408,6 +409,7 @@ try {
408
409
  else if (command === "seo") seo(args);
409
410
  else if (command === "ops") workflowCli("maggie_ops.py", args);
410
411
  else if (command === "deployment" && args[0] === "canary") workflowCli("maggie_deployment_canary.py", args.slice(1));
412
+ else if (command === "deployment" && args[0] === "readiness") workflowCli("maggie_deployment_readiness.py", args.slice(1));
411
413
  else if (command === "deployment") workflowCli("maggie_deployment.py", args);
412
414
  else if (command === "migration") workflowCli("maggie_migration.py", args);
413
415
  else if (command === "schedule") workflowCli("maggie_schedule.py", args);
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://maggie.noblox.app/contracts/deployment-readiness-v1.json",
4
+ "title": "Maggie deployment readiness evidence summary",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "status", "checks"],
7
+ "properties": {
8
+ "schemaVersion": {"const": "maggie-deployment-readiness.v1"},
9
+ "status": {"enum": ["passed", "failed", "inconclusive"]},
10
+ "checks": {
11
+ "type": "object",
12
+ "required": ["unitRegression", "packageSmoke", "browserEvidence", "renderedCanary", "deploymentPreflight"],
13
+ "additionalProperties": false,
14
+ "properties": {
15
+ "unitRegression": {"$ref": "#/$defs/check"},
16
+ "packageSmoke": {"$ref": "#/$defs/check"},
17
+ "browserEvidence": {"$ref": "#/$defs/check"},
18
+ "renderedCanary": {"$ref": "#/$defs/check"},
19
+ "deploymentPreflight": {"$ref": "#/$defs/check"}
20
+ }
21
+ }
22
+ },
23
+ "$defs": {
24
+ "check": {
25
+ "type": "object",
26
+ "required": ["state", "configured"],
27
+ "properties": {
28
+ "state": {"enum": ["passed", "failed", "inconclusive", "not-configured"]},
29
+ "configured": {"type": "boolean"},
30
+ "path": {"type": "string"},
31
+ "reason": {"type": "string"}
32
+ },
33
+ "additionalProperties": true
34
+ }
35
+ },
36
+ "additionalProperties": false
37
+ }
@@ -61,6 +61,12 @@ Dashboard browser checks must emit runtime evidence; a static component scan
61
61
  or JSON declaration is not proof that a screen mounted or that its data calls
62
62
  ran.
63
63
 
64
+ The first-party MaggieDash 0.2.4 distribution exposes these adapters through
65
+ its dashboard UI v3 contract: a read-only Activity workspace and an atomic,
66
+ locale-aware Navigation workspace. The reusable UI accepts the standard
67
+ `events`/`occurredAt` and `items`/`destination` shapes, while also reading the
68
+ legacy host response aliases used by existing adapters during migration.
69
+
64
70
  ## Status model
65
71
 
66
72
  Content transitions are explicit:
@@ -61,6 +61,15 @@ Supported operations are distinct: `translate`, `polish`, `rewrite`,
61
61
  source. A source revision change marks dependent translations stale. Fallback
62
62
  content is never indexable.
63
63
 
64
+ `polish` and `rewrite` validation includes deterministic output checks. Polish
65
+ must keep the source language, change the copy, preserve protected fact tokens,
66
+ and show a clarity signal (shorter maximum sentence, clearer sentence
67
+ segmentation, placeholder reduction, or explicit quality evidence). Rewrite
68
+ must change the copy and its observable structure, while preserving protected
69
+ fact tokens. These are conservative heuristics, not semantic equivalence
70
+ proof: every result contains `quality.humanReview.required: true` and marks
71
+ semantic equivalence as `not-certified` until a named reviewer approves it.
72
+
64
73
  The following are protected by default: price, currency, rating, provider
65
74
  facts, booking URL, legal/health claims, content ID, slug, canonical owner,
66
75
  and translation group. Changes require structured approval and evidence.
@@ -14,9 +14,12 @@ metadata:
14
14
  changes; translate and polish preserve meaning; localise enables market
15
15
  adaptation. A host generation adapter or the authoring agent must apply these
16
16
  instructions when producing copy. Planning does not invoke a model or generate
17
- translations. Validation currently checks mode consistency and rewrite
18
- permission, not whether prose was actually restructured. Review the produced
19
- copy against the requested operation before approval.
17
+ translations. Validation now applies deterministic operation-specific checks to
18
+ polish and rewrite outputs: source/output text must be present, protected fact
19
+ tokens must remain unchanged, polish must show a clarity signal, and rewrite
20
+ must change observable structure. These checks are conservative heuristics,
21
+ not semantic equivalence proof. Every result requires named human review and
22
+ reports `semanticEquivalence: not-certified` until approval.
20
23
 
21
24
  ### Resumable draft generation
22
25
 
@@ -46,6 +46,15 @@ owns the framework route, email/password session middleware, database, media
46
46
  storage, provider credentials, and `/api/maggie/*` adapter endpoints. Read the
47
47
  MaggieDash host adapter contract before adding a new framework adapter.
48
48
 
49
+ MaggieDash 0.2.4 adds two reusable operator workspaces. Activity is a read-only
50
+ view backed by `/api/maggie/activity.json`; it supports actor/action filters,
51
+ refresh, and a safe unavailable state. Navigation is an ordered,
52
+ locale/location-aware editor backed by `/api/maggie/navigation.json`; it saves
53
+ the full menu atomically and requires the host to keep published markup as its
54
+ fallback. The host owns authentication, event writes, menu persistence,
55
+ translation behavior, and validation. Use the dashboard UI v3 contract for the
56
+ required route and capability declarations.
57
+
49
58
  For a built/deployed host, an agent must use the host's validated content
50
59
  bridge rather than editing release files or receiving database credentials:
51
60
 
@@ -37,6 +37,31 @@ visitor-facing files. The canary must include screenshots, zero console or
37
37
  network errors, and zero placeholder matches. Query-driven routes belong in
38
38
  behavior/API checks, not static byte baselines.
39
39
 
40
+ ## Release readiness evidence
41
+
42
+ Unit regression is necessary but does not prove runtime release readiness. The
43
+ readiness command keeps five evidence slots separate: dependency-free unit
44
+ regression, package smoke, host browser evidence, rendered canary, and
45
+ deployment preflight:
46
+
47
+ ```bash
48
+ python3 tools/tests/run_regression.py \
49
+ --report .maggie/verification/unit-regression.json
50
+ maggie deployment readiness --project . \
51
+ --package-report .maggie/verification/package-smoke.json \
52
+ --browser-report .maggie/verification/browser-evidence.json \
53
+ --rendered-canary .maggie/deployment-canary.json \
54
+ --deployment-preflight .maggie/release-preflight.json \
55
+ --output .maggie/deployment-readiness.json
56
+ ```
57
+
58
+ The report follows `maggie-deployment-readiness.v1`. A failed evidence file
59
+ returns `failed`; a missing or unavailable host/browser adapter returns
60
+ `inconclusive`; only five passing evidence slots return `passed`. Maggie does
61
+ not fabricate browser, rendered, or deployment evidence and does not deploy
62
+ from this command. See
63
+ [`readiness-v1.schema.json`](../../bundled-contracts/maggie-deployment/readiness-v1.schema.json).
64
+
40
65
  ## Automatic memory hook
41
66
 
42
67
  Follow [Maggie Memory Hook](../../references/memory-hook.md) at invocation and completion.
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env python3
2
+ """Summarize release evidence without confusing tests with runtime readiness."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ from typing import Any, Callable
12
+
13
+
14
+ CHECKS = (
15
+ ("unitRegression", "unit_report"),
16
+ ("packageSmoke", "package_report"),
17
+ ("browserEvidence", "browser_report"),
18
+ ("renderedCanary", "rendered_canary"),
19
+ ("deploymentPreflight", "deployment_preflight"),
20
+ )
21
+
22
+
23
+ def display_path(path: Path, project: Path) -> str:
24
+ try:
25
+ return str(path.resolve().relative_to(project.resolve()))
26
+ except ValueError:
27
+ return path.name
28
+
29
+
30
+ def validate_unit(payload: dict[str, Any]) -> tuple[bool, str]:
31
+ if payload.get("schemaVersion") != "maggie-regression.v1":
32
+ return False, "unit evidence schemaVersion is unsupported"
33
+ if payload.get("status") != "passed" or not isinstance(payload.get("suites"), int) or payload["suites"] < 1:
34
+ return False, "unit evidence must report a passing suite count"
35
+ if payload.get("skipped", 0) != 0:
36
+ return False, "unit evidence contains skipped suites"
37
+ return True, "dependency-free regression suites passed"
38
+
39
+
40
+ def validate_package(payload: dict[str, Any]) -> tuple[bool, str]:
41
+ if payload.get("schemaVersion") != "maggie-package-smoke.v1":
42
+ return False, "package evidence schemaVersion is unsupported"
43
+ checks = payload.get("checks")
44
+ if payload.get("passed") is not True or not isinstance(checks, list) or not checks:
45
+ return False, "package evidence must contain passing smoke checks"
46
+ return True, "package smoke checks passed"
47
+
48
+
49
+ def validate_browser(payload: dict[str, Any]) -> tuple[bool, str]:
50
+ if payload.get("schemaVersion") != "maggie-browser-evidence.v1":
51
+ return False, "browser evidence schemaVersion is unsupported"
52
+ routes = payload.get("routes")
53
+ if payload.get("passed") is not True or not isinstance(routes, list) or not routes:
54
+ return False, "browser evidence must contain a passing non-empty route set"
55
+ for route in routes:
56
+ if not isinstance(route, dict) or not route.get("route") or not route.get("screenshot"):
57
+ return False, "browser evidence routes need a route and screenshot"
58
+ if route.get("consoleErrors") or route.get("networkErrors") or route.get("missingAssets"):
59
+ return False, "browser evidence contains runtime errors"
60
+ return True, "host browser evidence passed"
61
+
62
+
63
+ def validate_canary(payload: dict[str, Any]) -> tuple[bool, str]:
64
+ if payload.get("schemaVersion") != "maggie-deployment-canary.v1":
65
+ return False, "rendered canary schemaVersion is unsupported"
66
+ if payload.get("status") != "pass":
67
+ return False, "rendered canary did not pass"
68
+ rendered = payload.get("rendered")
69
+ if not isinstance(rendered, dict) or not isinstance(rendered.get("report"), dict):
70
+ return False, "rendered canary is missing browser-rendered report evidence"
71
+ return True, "edge and rendered canary passed"
72
+
73
+
74
+ def validate_preflight(payload: dict[str, Any]) -> tuple[bool, str]:
75
+ if payload.get("schemaVersion") not in {"1.0", "maggie-release-preflight.v1"}:
76
+ return False, "deployment preflight schemaVersion is unsupported"
77
+ if payload.get("passed") is not True:
78
+ return False, "deployment preflight contains a failed gate"
79
+ gates = payload.get("gates")
80
+ if not isinstance(gates, list) or not gates:
81
+ return False, "deployment preflight has no gate results"
82
+ return True, "deployment preflight gates passed"
83
+
84
+
85
+ VALIDATORS: dict[str, Callable[[dict[str, Any]], tuple[bool, str]]] = {
86
+ "unitRegression": validate_unit,
87
+ "packageSmoke": validate_package,
88
+ "browserEvidence": validate_browser,
89
+ "renderedCanary": validate_canary,
90
+ "deploymentPreflight": validate_preflight,
91
+ }
92
+
93
+
94
+ def inspect_check(name: str, path: Path, project: Path) -> dict[str, Any]:
95
+ result: dict[str, Any] = {"state": "inconclusive", "configured": path.is_file(), "path": display_path(path, project)}
96
+ if not path.is_file():
97
+ result["reason"] = "required adapter evidence is unavailable"
98
+ return result
99
+ try:
100
+ payload = json.loads(path.read_text(encoding="utf-8"))
101
+ except (OSError, json.JSONDecodeError) as error:
102
+ result.update({"state": "failed", "reason": f"evidence is not valid JSON: {error}"})
103
+ return result
104
+ if not isinstance(payload, dict):
105
+ result.update({"state": "failed", "reason": "evidence root must be an object"})
106
+ return result
107
+ valid, reason = VALIDATORS[name](payload)
108
+ result.update({"state": "passed" if valid else "failed", "reason": reason})
109
+ return result
110
+
111
+
112
+ def readiness(project: Path, paths: dict[str, Path], output: Path | None = None) -> int:
113
+ checks = {name: inspect_check(name, path, project) for name, path in paths.items()}
114
+ states = {item["state"] for item in checks.values()}
115
+ status = "failed" if "failed" in states else "inconclusive" if "inconclusive" in states else "passed"
116
+ report = {
117
+ "schemaVersion": "maggie-deployment-readiness.v1",
118
+ "generatedAt": datetime.now(timezone.utc).isoformat(),
119
+ "project": display_path(project, project),
120
+ "status": status,
121
+ "checks": checks,
122
+ "nextAction": "resolve failed evidence before release" if status == "failed" else "collect the missing host adapter evidence" if status == "inconclusive" else "all declared readiness evidence passed",
123
+ }
124
+ if output:
125
+ destination = output if output.is_absolute() else project / output
126
+ destination.parent.mkdir(parents=True, exist_ok=True)
127
+ destination.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
128
+ report["output"] = display_path(destination, project)
129
+ print(json.dumps(report, indent=2, ensure_ascii=False))
130
+ return 0 if status == "passed" else 1
131
+
132
+
133
+ def main() -> int:
134
+ parser = argparse.ArgumentParser(description=__doc__)
135
+ parser.add_argument("--project", type=Path, default=Path.cwd())
136
+ parser.add_argument("--unit-report", type=Path, default=Path(".maggie/verification/unit-regression.json"))
137
+ parser.add_argument("--package-report", type=Path, default=Path(".maggie/verification/package-smoke.json"))
138
+ parser.add_argument("--browser-report", type=Path, default=Path(".maggie/verification/browser-evidence.json"))
139
+ parser.add_argument("--rendered-canary", type=Path, default=Path(".maggie/deployment-canary.json"))
140
+ parser.add_argument("--deployment-preflight", type=Path, default=Path(".maggie/release-preflight.json"))
141
+ parser.add_argument("--output", type=Path)
142
+ args = parser.parse_args()
143
+ project = args.project.resolve()
144
+ if not project.is_dir():
145
+ parser.error(f"project directory does not exist: {project}")
146
+ paths = {
147
+ name: (getattr(args, option).resolve() if getattr(args, option).is_absolute() else project / getattr(args, option))
148
+ for name, option in CHECKS
149
+ }
150
+ try:
151
+ return readiness(project, paths, args.output)
152
+ except (OSError, ValueError) as error:
153
+ print(f"BLOCKED: maggie deployment readiness: {error}", file=sys.stderr)
154
+ return 2
155
+
156
+
157
+ if __name__ == "__main__":
158
+ raise SystemExit(main())
@@ -12,7 +12,7 @@ from datetime import datetime, timezone
12
12
  from pathlib import Path
13
13
 
14
14
  sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "runtime"))
15
- from content_localization import LANGUAGES, MARKETS, OPERATIONS, parse_locale, protected_field_changes, stale, valid_locale, validate_translation
15
+ from content_localization import LANGUAGES, MARKETS, OPERATIONS, parse_locale, protected_field_changes, stale, valid_locale, validate_operation_quality, validate_translation
16
16
  from localization_runner import generate, process_adapter
17
17
 
18
18
 
@@ -250,6 +250,8 @@ def validate_job(path: Path, source_path: Path | None = None, render_path: Path
250
250
  errors.append("generationContract mode must match operation")
251
251
  if job.get("operation") == "rewrite" and not generation.get("allowStructuralRewrite"):
252
252
  errors.append("rewrite requires structural rewrite permission")
253
+ if job.get("operation") == "polish" and job.get("sourceLanguage") != job.get("targetLanguage"):
254
+ errors.append("polish must keep sourceLanguage and targetLanguage identical")
253
255
  errors.extend(validate_translation({
254
256
  "contentId": job.get("contentId"), "lang": job.get("targetLanguage"),
255
257
  "locale": job.get("targetLocale"), "translationGroupId": job.get("translationGroupId", f"tg-{job.get('contentId')}"),
@@ -260,6 +262,8 @@ def validate_job(path: Path, source_path: Path | None = None, render_path: Path
260
262
  })
261
263
  }, expected_lang=job.get("targetLanguage")))
262
264
  errors.extend(f"protected field changed without approval: {field}" for field in protected_field_changes(job.get("content", {}), translation))
265
+ quality_errors, quality = validate_operation_quality(job.get("content", {}), translation, job.get("operation", ""), job.get("review"))
266
+ errors.extend(quality_errors)
263
267
  if source_path:
264
268
  errors.extend(validate_source(source_path, job.get("sourceRevision")))
265
269
  source = load(source_path)
@@ -269,7 +273,7 @@ def validate_job(path: Path, source_path: Path | None = None, render_path: Path
269
273
  errors.append("translation sourceStringIds must link every extracted source string")
270
274
  if render_path:
271
275
  errors.extend(validate_render_report(render_path))
272
- return {"passed": not errors, "errors": errors, "jobId": job.get("jobId"), "status": job.get("status")}
276
+ return {"passed": not errors, "errors": errors, "jobId": job.get("jobId"), "status": job.get("status"), "quality": quality}
273
277
 
274
278
 
275
279
  def preview(path: Path) -> int:
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections import Counter
5
6
  import re
6
7
  from typing import Any, Iterable
7
8
 
@@ -17,6 +18,10 @@ MARKETS = {"global", "uk", "us"}
17
18
  STATUSES = {"missing", "draft", "machine_translated", "needs_review", "approved", "published", "archived"}
18
19
  OPERATIONS = {"translate", "polish", "rewrite", "localise", "rebrand", "manual_edit"}
19
20
  PROTECTED_FIELDS = {"price", "currency", "rating", "provider", "providerId", "bookingUrl", "paymentUrl", "legalClaims", "healthClaims", "contentId", "slug", "canonicalUrl", "translationGroupId"}
21
+ QUALITY_OPERATIONS = {"polish", "rewrite"}
22
+ QUALITY_META_FIELDS = {"identity", "contentId", "contentType", "sourceRevision", "translationStatus", "isIndexable", "provenance", "sourceStringIds", "qualityEvidence", "humanReviewStatus"}
23
+ FACT_TOKEN_PATTERN = re.compile(r"https?://[^\s<>]+|[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}|(?:£|\$|€|¥|₹)\s*\d[\d,.]*|\b\d[\d,.]*%|\b\d[\d,.]*\b", re.UNICODE)
24
+ PLACEHOLDER_PATTERN = re.compile(r"\{\{[^}]+\}\}|\$\{[^}]+\}|\b(?:TODO|TBD|lorem ipsum)\b", re.IGNORECASE)
20
25
 
21
26
 
22
27
  class TranslationIndex:
@@ -162,3 +167,135 @@ def protected_field_changes(source: Any, translation: Any) -> list[str]:
162
167
  if not isinstance(source, dict) or not isinstance(translation, dict):
163
168
  return []
164
169
  return [field for field in sorted(PROTECTED_FIELDS) if field in source and field in translation and source[field] != translation[field]]
170
+
171
+
172
+ def _copy_payload(value: Any) -> Any:
173
+ """Remove identity/approval metadata before comparing editorial structure."""
174
+ if isinstance(value, dict):
175
+ return {
176
+ key: _copy_payload(item)
177
+ for key, item in value.items()
178
+ if key not in QUALITY_META_FIELDS and key not in PROTECTED_FIELDS
179
+ }
180
+ if isinstance(value, list):
181
+ return [_copy_payload(item) for item in value]
182
+ return value
183
+
184
+
185
+ def _copy_text(value: Any) -> str:
186
+ if isinstance(value, dict):
187
+ return "\n".join(part for part in (_copy_text(item) for item in value.values()) if part)
188
+ if isinstance(value, list):
189
+ return "\n".join(part for part in (_copy_text(item) for item in value) if part)
190
+ return str(value).strip() if isinstance(value, str) else ""
191
+
192
+
193
+ def _copy_metrics(text: str) -> dict[str, int | bool]:
194
+ normalized = text.strip()
195
+ sentences = [item for item in re.split(r"[.!?。!?]+", normalized) if item.strip()]
196
+ words = re.findall(r"\w+(?:['’\-]\w+)*", normalized, re.UNICODE)
197
+ sentence_lengths = [len(re.findall(r"\w+", item, re.UNICODE)) for item in sentences]
198
+ return {
199
+ "characters": len(normalized),
200
+ "words": len(words),
201
+ "sentences": len(sentences),
202
+ "paragraphs": len([item for item in re.split(r"\n\s*\n", normalized) if item.strip()]),
203
+ "headings": len(re.findall(r"(?im)^\s{0,3}(?:#{1,6}\s+|<h[1-6]\b)", normalized)),
204
+ "listItems": len(re.findall(r"(?im)^\s*(?:[-*+]\s+|\d+[.)]\s+|<li\b)", normalized)),
205
+ "maxSentenceWords": max(sentence_lengths, default=0),
206
+ "placeholders": len(PLACEHOLDER_PATTERN.findall(normalized)),
207
+ "repeatedWhitespace": len(re.findall(r"[ \t]{2,}", normalized)),
208
+ }
209
+
210
+
211
+ def _fact_tokens(text: str) -> Counter[str]:
212
+ return Counter(token.casefold().rstrip(".,;:)") for token in FACT_TOKEN_PATTERN.findall(text))
213
+
214
+
215
+ def _structure_signature(value: Any) -> tuple[Any, ...]:
216
+ if isinstance(value, dict):
217
+ return ("object", tuple((key, _structure_signature(item)) for key, item in sorted(value.items())))
218
+ if isinstance(value, list):
219
+ return ("list", len(value), tuple(_structure_signature(item) for item in value))
220
+ if isinstance(value, str):
221
+ metrics = _copy_metrics(value)
222
+ return ("text", metrics["paragraphs"], metrics["headings"], metrics["listItems"], metrics["sentences"])
223
+ if value is None:
224
+ return ("null",)
225
+ return (type(value).__name__,)
226
+
227
+
228
+ def validate_operation_quality(source: Any, translation: Any, operation: str, review: Any = None) -> tuple[list[str], dict[str, Any]]:
229
+ """Run conservative, dependency-free checks for polish and rewrite jobs.
230
+
231
+ These checks cover observable copy structure and protected fact tokens. They
232
+ intentionally do not claim semantic equivalence; every applicable result
233
+ carries a mandatory human-review signal.
234
+ """
235
+ report: dict[str, Any] = {
236
+ "operation": operation,
237
+ "applicable": operation in QUALITY_OPERATIONS,
238
+ "method": "deterministic-copy-heuristics",
239
+ }
240
+ if operation not in QUALITY_OPERATIONS:
241
+ return [], report
242
+ source_copy = _copy_payload(source)
243
+ translation_copy = _copy_payload(translation)
244
+ source_text = _copy_text(source_copy)
245
+ translation_text = _copy_text(translation_copy)
246
+ source_metrics = _copy_metrics(source_text)
247
+ translation_metrics = _copy_metrics(translation_text)
248
+ source_facts = _fact_tokens(source_text)
249
+ translation_facts = _fact_tokens(translation_text)
250
+ changed_fact_count = sum((source_facts - translation_facts).values()) + sum((translation_facts - source_facts).values())
251
+ normalized_source = re.sub(r"\s+", " ", source_text).strip().casefold()
252
+ normalized_translation = re.sub(r"\s+", " ", translation_text).strip().casefold()
253
+ structure_changed = _structure_signature(source_copy) != _structure_signature(translation_copy)
254
+ clarity_signal = (
255
+ translation_metrics["maxSentenceWords"] < source_metrics["maxSentenceWords"]
256
+ or translation_metrics["sentences"] > source_metrics["sentences"]
257
+ or translation_metrics["repeatedWhitespace"] < source_metrics["repeatedWhitespace"]
258
+ or translation_metrics["placeholders"] < source_metrics["placeholders"]
259
+ )
260
+ declared_clarity = isinstance(translation, dict) and isinstance(translation.get("qualityEvidence"), dict) and translation["qualityEvidence"].get("clarityImproved") is True
261
+ human_review = review if isinstance(review, dict) else {}
262
+ review_approved = human_review.get("decision") == "approve" and bool(human_review.get("reviewer"))
263
+ errors: list[str] = []
264
+ if not source_text:
265
+ errors.append(f"{operation} quality requires source copy text")
266
+ if not translation_text:
267
+ errors.append(f"{operation} quality requires generated copy text")
268
+ if changed_fact_count:
269
+ errors.append(f"{operation} output changed {changed_fact_count} protected fact token(s)")
270
+ if operation == "polish":
271
+ if normalized_source and normalized_source == normalized_translation:
272
+ errors.append("polish output is unchanged")
273
+ if translation_metrics["placeholders"] > source_metrics["placeholders"]:
274
+ errors.append("polish output introduced unresolved placeholders")
275
+ if source_text and translation_text and not (clarity_signal or declared_clarity):
276
+ errors.append("polish output has no deterministic clarity-improvement signal")
277
+ if operation == "rewrite":
278
+ if normalized_source and normalized_source == normalized_translation:
279
+ errors.append("rewrite output is unchanged")
280
+ if source_text and translation_text and not structure_changed:
281
+ errors.append("rewrite output did not change copy structure")
282
+ report.update({
283
+ "source": source_metrics,
284
+ "output": translation_metrics,
285
+ "checks": {
286
+ "nonEmpty": bool(source_text and translation_text),
287
+ "changed": bool(normalized_source != normalized_translation),
288
+ "protectedFactsPreserved": changed_fact_count == 0,
289
+ "structureChanged": structure_changed,
290
+ "clarityImproved": bool(clarity_signal or declared_clarity) if operation == "polish" else None,
291
+ "clarityEvidence": "deterministic" if clarity_signal else "declared" if declared_clarity else "missing",
292
+ },
293
+ "humanReview": {
294
+ "required": True,
295
+ "status": "approved" if review_approved else "required",
296
+ "semanticEquivalence": "not-certified",
297
+ "reason": "heuristics do not replace human meaning review",
298
+ },
299
+ "status": "passed" if not errors else "failed",
300
+ })
301
+ return errors, report
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topy-ai/maggie",
3
- "version": "0.7.19",
3
+ "version": "0.7.21",
4
4
  "description": "Install and manage Maggie Skills for AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -61,6 +61,15 @@ Supported operations are distinct: `translate`, `polish`, `rewrite`,
61
61
  source. A source revision change marks dependent translations stale. Fallback
62
62
  content is never indexable.
63
63
 
64
+ `polish` and `rewrite` validation includes deterministic output checks. Polish
65
+ must keep the source language, change the copy, preserve protected fact tokens,
66
+ and show a clarity signal (shorter maximum sentence, clearer sentence
67
+ segmentation, placeholder reduction, or explicit quality evidence). Rewrite
68
+ must change the copy and its observable structure, while preserving protected
69
+ fact tokens. These are conservative heuristics, not semantic equivalence
70
+ proof: every result contains `quality.humanReview.required: true` and marks
71
+ semantic equivalence as `not-certified` until a named reviewer approves it.
72
+
64
73
  The following are protected by default: price, currency, rating, provider
65
74
  facts, booking URL, legal/health claims, content ID, slug, canonical owner,
66
75
  and translation group. Changes require structured approval and evidence.