@topy-ai/maggie 0.7.18 → 0.7.19

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
@@ -76,6 +76,24 @@ maggie qa summary --project . --run <run-id>
76
76
  The QA workflow stores secret-free run state under `.maggie/qa-runs/` and
77
77
  keeps project-specific scenarios and evidence outside the npm package.
78
78
 
79
+ Dashboard and documentation audits are provider-neutral and keep host data
80
+ behind explicit evidence files:
81
+
82
+ ```bash
83
+ maggie dash api-contract --spec .maggie/openapi.json
84
+ maggie dash api-contract --spec https://example.test/openapi.json
85
+ maggie dash schema-audit --inventory .maggie/schema-inventory.json --fail-on-unread
86
+ maggie dash ui runtime-validate --evidence .maggie/dashboard-runtime.json
87
+ maggie docs audit --project . --docs-dir docs --output docs/documentation-audit.json
88
+ ```
89
+
90
+ The API checker accepts a local JSON file or a public HTTPS URL and validates
91
+ the response in memory. Schema audits use host-provided table, row-count, and
92
+ reader evidence; they never guess from a production database. Runtime
93
+ evidence proves that a dashboard screen mounted and executed its checks, so a
94
+ static component scan cannot pass by itself. MaggieDash hosts may also expose
95
+ the provider-neutral activity-log and managed-navigation contracts.
96
+
79
97
  The current package also includes reusable safeguards from the latest feedback
80
98
  review: `maggie dash api-contract` checks declared request and 2xx response
81
99
  shapes; `maggie blog check-gate`/`approve` enforces review before publish;
@@ -151,7 +169,10 @@ maggie dash transition ... # explicit content approval transition
151
169
  maggie dash variant ... # service variant create/review/preview/publish
152
170
  maggie dash sections ... # field fan-out, locale, binding, media, copy, identity keys
153
171
  maggie dash inventory ... # disjoint renderer/page-kind inventory + coverage
154
- maggie dash api-contract ... # declared API body and 2xx response contract
172
+ maggie dash api-contract ... # local/live declared API body and 2xx contract
173
+ maggie dash schema-audit ... # host schema reader-evidence audit
174
+ maggie dash ui runtime-validate ... # mounted dashboard runtime evidence
175
+ maggie docs audit ... # documentation hygiene audit
155
176
  maggie agent-content write ... # host-authorized, origin-bound content bridge
156
177
  maggie verification coverage ... # changed surface/locale evidence gate
157
178
  maggie clone ... # authorized homepage capture
@@ -284,8 +305,8 @@ artifact schemas.
284
305
  Recommended upgrade sequence for the current release:
285
306
 
286
307
  ```bash
287
- npx @topy-ai/maggie@0.7.18 update --project . --force
288
- npx @topy-ai/maggie@0.7.18 cleanup --project .
308
+ npx @topy-ai/maggie@0.7.19 update --project . --force
309
+ npx @topy-ai/maggie@0.7.19 cleanup --project .
289
310
  ```
290
311
 
291
312
  Maintainers should pass npm credentials through the repository helper, never
@@ -295,7 +316,10 @@ as a command-line argument:
295
316
  node scripts/publish-npm.mjs --maggie-env-file ../.env
296
317
  ```
297
318
 
298
- The 0.7.18 workflow adds staged/untracked changed-surface detection, scenario
319
+ The 0.7.19 workflow adds documentation hygiene audits, live URL API contract
320
+ validation, host-supplied schema reader audits, dashboard runtime evidence,
321
+ and provider-neutral activity-log and managed-navigation contracts. The 0.7.18
322
+ workflow adds staged/untracked changed-surface detection, scenario
299
323
  QA release integration, market/locale-aware service variant routes and
300
324
  reciprocal hreflang, provider capability matrices with fixture evidence, and
301
325
  feedback fixed-proof/path-privacy gates. The 0.7.17 workflow adds sanitized database-backed blog gate adapters, deployed
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.18 init --agent all
11
+ npx @topy-ai/maggie@0.7.19 init --agent all
12
12
  npx @topy-ai/maggie doctor --project .
13
13
  ```
14
14
 
@@ -25,6 +25,8 @@ 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、
29
+ dashboard runtime evidence,以及 activity-log 和 managed-navigation contracts。
28
30
  0.7.18 加入 staged/untracked changed-surface release gate、QA run 與 release
29
31
  整合、market/locale-aware service variant route、provider capability matrix
30
32
  fixture evidence,以及 feedback fixed-proof/path privacy gate。0.7.16 補強 review gate 的 CI exit code 與 IndexNow retry regression;
package/bin/maggie.js CHANGED
@@ -61,6 +61,9 @@ Usage:
61
61
  maggie dash sections <validate|prompt|keys|remap-translations|fanout-validate|locale-validate|variant-copy-validate|media-validate|bindings-validate|idempotency-validate|reconcile> [options]
62
62
  maggie dash inventory --pages-file FILE [--require-page-kinds] [--require-source-coverage]
63
63
  maggie dash api-contract --spec FILE
64
+ maggie dash api-contract --spec https://example.test/openapi.json
65
+ maggie dash schema-audit --inventory FILE [--fail-on-unread]
66
+ maggie dash ui runtime-validate --evidence FILE
64
67
  maggie dash components-audit --bindings-file FILE --sections-file FILE --pages-file FILE
65
68
  maggie dash status --project PATH
66
69
  maggie dash migrate --project PATH --confirm
@@ -301,6 +304,11 @@ function service(args) {
301
304
  process.exitCode = result.status ?? 1;
302
305
  }
303
306
 
307
+ function docs(args) {
308
+ if (args[0] && args[0] !== "audit") throw new Error("docs command must be audit");
309
+ workflowCli("maggie_docs.py", args);
310
+ }
311
+
304
312
  function seo(args) {
305
313
  const command = args[0];
306
314
  const scripts = { performance: "maggie_performance.py", images: "maggie_images.py", sitemap: "maggie_sitemap.py", indexnow: "maggie_indexnow.py", "social-cards": "maggie_social_cards.py", "head-tags": "maggie_head_tags.py" };
@@ -396,6 +404,7 @@ try {
396
404
  else if (command === "auth") workflowCli("maggie_auth.py", args);
397
405
  else if (command === "blog") workflowCli("maggie_blog.py", args);
398
406
  else if (command === "service") service(args);
407
+ else if (command === "docs") docs(args);
399
408
  else if (command === "seo") seo(args);
400
409
  else if (command === "ops") workflowCli("maggie_ops.py", args);
401
410
  else if (command === "deployment" && args[0] === "canary") workflowCli("maggie_deployment_canary.py", args.slice(1));
@@ -42,6 +42,25 @@ but they may not change the required identity, status, provenance, or approval
42
42
  fields. Unknown fields must be preserved or reported as unsupported during
43
43
  import.
44
44
 
45
+ ## Operational UI contracts
46
+
47
+ - [`activity-log-v1.json`](activity-log-v1.json) defines append-only activity
48
+ events, actor/action taxonomy, and the metadata boundary;
49
+ - [`navigation-v1.json`](navigation-v1.json) defines locale-aware managed menu
50
+ items with stable ordering, nesting, and safe destinations;
51
+ - [`dashboard-runtime-v1.json`](dashboard-runtime-v1.json) defines evidence
52
+ that an admin screen mounted, ran its checks, and completed its data calls.
53
+
54
+ These are adapter-facing interfaces, not database schemas. A host may use
55
+ PostgreSQL, SQLite, or another first-party store. Activity recording happens
56
+ after a successful caller write, uses the same transaction/client where
57
+ available, redacts credential-like metadata, and must never make the business
58
+ operation fail. Retention/pruning is explicit. Navigation reads fall back to
59
+ site markup when no published menu exists or the adapter is unavailable.
60
+ Dashboard browser checks must emit runtime evidence; a static component scan
61
+ or JSON declaration is not proof that a screen mounted or that its data calls
62
+ ran.
63
+
45
64
  ## Status model
46
65
 
47
66
  Content transitions are explicit:
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://maggiedash.noblox.app/contracts/activity-log-v1.json",
4
+ "title": "MaggieDash first-party activity log contract v1",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "events"],
7
+ "properties": {
8
+ "schemaVersion": {"const": "maggiedash-activity-log.v1"},
9
+ "events": {
10
+ "type": "array",
11
+ "items": {
12
+ "type": "object",
13
+ "required": ["id", "occurredAt", "actor", "action"],
14
+ "properties": {
15
+ "id": {"type": "string", "minLength": 1},
16
+ "occurredAt": {"type": "string", "format": "date-time"},
17
+ "actor": {"type": "string", "minLength": 1},
18
+ "action": {"type": "string", "pattern": "^[a-z_]+\\.[a-z_]+$"},
19
+ "summary": {"type": "string"},
20
+ "metadata": {"type": "object", "additionalProperties": true}
21
+ },
22
+ "additionalProperties": false
23
+ }
24
+ }
25
+ },
26
+ "additionalProperties": false
27
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://maggiedash.noblox.app/contracts/dashboard-runtime-v1.json",
4
+ "title": "MaggieDash dashboard runtime evidence contract v1",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "screen", "route", "mounted", "checks"],
7
+ "properties": {
8
+ "schemaVersion": {"const": "maggiedash-dashboard-runtime.v1"},
9
+ "screen": {"type": "string", "minLength": 1},
10
+ "route": {"type": "string", "minLength": 1},
11
+ "mounted": {"const": true},
12
+ "checks": {
13
+ "type": "array",
14
+ "minItems": 1,
15
+ "items": {
16
+ "type": "object",
17
+ "required": ["name", "passed"],
18
+ "properties": {"name": {"type": "string", "minLength": 1}, "passed": {"type": "boolean"}},
19
+ "additionalProperties": false
20
+ }
21
+ },
22
+ "dataCalls": {
23
+ "type": "array",
24
+ "items": {
25
+ "type": "object",
26
+ "required": ["url", "ok"],
27
+ "properties": {"url": {"type": "string"}, "method": {"type": "string"}, "status": {"type": "integer"}, "ok": {"type": "boolean"}},
28
+ "additionalProperties": false
29
+ }
30
+ }
31
+ },
32
+ "additionalProperties": false
33
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://maggiedash.noblox.app/contracts/navigation-v1.json",
4
+ "title": "MaggieDash managed navigation contract v1",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "location", "locale", "items"],
7
+ "properties": {
8
+ "schemaVersion": {"const": "maggiedash-navigation.v1"},
9
+ "location": {"type": "string", "minLength": 1},
10
+ "locale": {"type": "string", "minLength": 2},
11
+ "items": {
12
+ "type": "array",
13
+ "items": {
14
+ "type": "object",
15
+ "required": ["id", "label", "destination", "order"],
16
+ "properties": {
17
+ "id": {"type": "string", "minLength": 1},
18
+ "label": {"type": "string", "minLength": 1},
19
+ "destination": {"type": "string", "minLength": 1},
20
+ "order": {"type": "integer", "minimum": 0},
21
+ "parentId": {"type": ["string", "null"]},
22
+ "newTab": {"type": "boolean"}
23
+ },
24
+ "additionalProperties": false
25
+ }
26
+ }
27
+ },
28
+ "additionalProperties": false
29
+ }
@@ -200,8 +200,21 @@ maggie dash sections bindings-validate --sections-file .maggie/sections.json \
200
200
  --references-file .maggie/reference-inventory.json
201
201
  maggie dash sections idempotency-validate --contract-file .maggie/reconcile-contract.json
202
202
 
203
- # Validate API body/response shapes before a client writes against an endpoint:
203
+ # Validate API body/response shapes before a client writes against an endpoint.
204
+ # The source can be a local JSON file or a public HTTPS URL:
204
205
  maggie dash api-contract --spec .maggie/openapi.json
206
+ maggie dash api-contract --spec https://example.test/openapi.json
207
+
208
+ # Find declared tables with no reader evidence; this never connects to a DB:
209
+ maggie dash schema-audit --inventory .maggie/schema-inventory.json \
210
+ --fail-on-unread
211
+
212
+ # Prove a browser check mounted the screen and executed its data calls:
213
+ maggie dash ui runtime-validate --evidence .maggie/dashboard-runtime.json
214
+
215
+ # Classify docs, show inbound references and completed progress trackers:
216
+ maggie docs audit --project . --docs-dir docs \
217
+ --output docs/documentation-audit.json
205
218
 
206
219
  # Classify every published page exactly once. `pageKind` describes the public
207
220
  # page family; `kind`/`rendererKind` describes how it is rendered. Keep these
@@ -219,6 +232,14 @@ ordinary, and blog routes, plus `sourceCoverage`. A host adapter must include
219
232
  code-rendered routes in the input or declare coverage incomplete; a zero
220
233
  database-row count is not evidence that no public routes exist.
221
234
 
235
+ The host can expose the optional `activity-log-v1` and `navigation-v1`
236
+ contracts through `/api/maggie/*` adapters. Activity events should be
237
+ append-only and non-blocking to the original write. Managed navigation should
238
+ be locale-aware, ordered, and safe to fall back to existing markup. Do not
239
+ claim a dashboard feature is tested from a static source scan alone: record a
240
+ `dashboard-runtime-v1` report with `mounted: true`, named checks, and the
241
+ actual data calls.
242
+
222
243
  When a host renders multiple shells, export a normalized head manifest and run
223
244
  `maggie seo head-tags audit`. This is the reusable boundary for detecting icon
224
245
  MIME drift, OG-image fallback drift, and verification-tag coverage; MaggieDash
@@ -25,7 +25,9 @@ from service_variants import ServiceVariantStore # noqa: E402
25
25
  from maggie_dash_ui import load_and_validate # noqa: E402
26
26
  from maggie_sections import catalogue, remap_translations, section_id_migration, validate_registry, copy_notes, validate_values, validate_fanout, reconcile_fields, validate_locale_coverage # noqa: E402
27
27
  from maggie_quality import validate_variant_copy, validate_media_uniqueness, classify_inventory, validate_bindings, validate_reconcile_contract # noqa: E402
28
- from maggie_api_contract import validate_api_contract # noqa: E402
28
+ from maggie_api_contract import load_api_contract_source, validate_api_contract # noqa: E402
29
+ from maggie_dash_runtime import validate_runtime_evidence # noqa: E402
30
+ from maggie_schema_audit import audit_schema_inventory # noqa: E402
29
31
  from route_imports import classify_bindings # noqa: E402
30
32
 
31
33
 
@@ -316,6 +318,11 @@ def command_cms(args: argparse.Namespace) -> int:
316
318
 
317
319
 
318
320
  def command_ui(args: argparse.Namespace) -> int:
321
+ if args.ui_command == "runtime-validate":
322
+ value = json.loads(Path(args.evidence).resolve().read_text(encoding="utf-8"))
323
+ result = validate_runtime_evidence(value)
324
+ emit(result)
325
+ return 0 if result["passed"] else 1
319
326
  result = load_and_validate(Path(args.contract).resolve(), [Path(value).resolve() for value in args.source])
320
327
  emit(result)
321
328
  return 0 if result["passed"] else 1
@@ -429,8 +436,15 @@ def command_inventory(args: argparse.Namespace) -> int:
429
436
 
430
437
 
431
438
  def command_api_contract(args: argparse.Namespace) -> int:
432
- value = json.loads(Path(args.spec).resolve().read_text(encoding="utf-8"))
433
- result = validate_api_contract(value)
439
+ value, source = load_api_contract_source(args.spec)
440
+ result = {**validate_api_contract(value), "source": source}
441
+ emit(result)
442
+ return 0 if result["passed"] else 1
443
+
444
+
445
+ def command_schema_audit(args: argparse.Namespace) -> int:
446
+ value = json.loads(Path(args.inventory).resolve().read_text(encoding="utf-8"))
447
+ result = audit_schema_inventory(value, fail_on_unread=args.fail_on_unread)
434
448
  emit(result)
435
449
  return 0 if result["passed"] else 1
436
450
 
@@ -537,6 +551,9 @@ def parser() -> argparse.ArgumentParser:
537
551
  ui_validate.add_argument("--contract", required=True)
538
552
  ui_validate.add_argument("--source", action="append", default=[])
539
553
  ui_validate.set_defaults(func=command_ui)
554
+ ui_runtime = ui_sub.add_parser("runtime-validate", help="validate evidence that a dashboard screen mounted and ran")
555
+ ui_runtime.add_argument("--evidence", required=True)
556
+ ui_runtime.set_defaults(func=command_ui)
540
557
  sections = sub.add_parser("sections", help="validate section registry and stable section identities")
541
558
  sections_sub = sections.add_subparsers(dest="sections_command", required=True)
542
559
  sections_validate = sections_sub.add_parser("validate")
@@ -583,9 +600,13 @@ def parser() -> argparse.ArgumentParser:
583
600
  inventory.add_argument("--require-page-kinds", action="store_true", help="fail when a published page has no pageKind")
584
601
  inventory.add_argument("--require-source-coverage", action="store_true", help="fail unless sourceCoverage.complete is true")
585
602
  inventory.set_defaults(func=command_inventory)
586
- api_contract = sub.add_parser("api-contract", help="validate declared request and success-response schemas")
587
- api_contract.add_argument("--spec", required=True, help="OpenAPI-like JSON document")
603
+ api_contract = sub.add_parser("api-contract", help="validate a local or live declared request and success-response contract")
604
+ api_contract.add_argument("--spec", required=True, help="local JSON file or HTTPS URL (loopback HTTP is allowed for development)")
588
605
  api_contract.set_defaults(func=command_api_contract)
606
+ schema_audit = sub.add_parser("schema-audit", help="find declared tables with missing reader evidence")
607
+ schema_audit.add_argument("--inventory", required=True, help="host-produced schema inventory JSON")
608
+ schema_audit.add_argument("--fail-on-unread", action="store_true")
609
+ schema_audit.set_defaults(func=command_schema_audit)
589
610
  variant = sub.add_parser("variant", help="manage service variant lifecycle")
590
611
  variant_sub = variant.add_subparsers(dest="variant_command", required=True)
591
612
  create = variant_sub.add_parser("create"); create.add_argument("--project", default="."); create.add_argument("--service-id", required=True); create.add_argument("--variant-id", required=True); create.add_argument("--variant-type", required=True); create.add_argument("--locale", required=True); create.add_argument("--market", required=True); create.add_argument("--slug", required=True); create.add_argument("--title", required=True); create.add_argument("--facts", required=True); create.add_argument("--source-revision", required=True); create.add_argument("--canonical-variant-id"); create.add_argument("--cluster-link", action="append", default=[]); create.add_argument("--layout-family", default="service-default"); create.add_argument("--confirm", action="store_true")
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env python3
2
+ """Audit project documentation for stale trackers and unreferenced files."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import re
9
+ import subprocess
10
+ from pathlib import Path
11
+
12
+
13
+ DOCUMENT_EXTENSIONS = {".md", ".mdx", ".json", ".yaml", ".yml"}
14
+ SKIP_PARTS = {".git", "node_modules", "dist", "build", ".next"}
15
+ COMPLETED_RE = re.compile(r"(?i)\b(?:done|complete|completed|shipped|resolved|closed)\b")
16
+ TRACKER_RE = re.compile(r"(?i)(?:plan|task|progress|roadmap|todo)")
17
+ RECORD_RE = re.compile(r"(?i)(?:audit|report|evidence|log|feedback|record)")
18
+
19
+
20
+ def _files(root: Path) -> list[Path]:
21
+ return sorted(
22
+ path for path in root.rglob("*")
23
+ if path.is_file() and path.suffix.casefold() in DOCUMENT_EXTENSIONS
24
+ and not any(part in SKIP_PARTS for part in path.relative_to(root).parts)
25
+ )
26
+
27
+
28
+ def _category(path: Path) -> str:
29
+ if path.suffix.casefold() in {".json", ".yaml", ".yml"}:
30
+ return "data"
31
+ name = path.stem
32
+ if TRACKER_RE.search(name):
33
+ return "plan"
34
+ if RECORD_RE.search(name):
35
+ return "record"
36
+ return "reference"
37
+
38
+
39
+ def _identity(path: Path, text: str) -> str:
40
+ heading = next((line.strip().lstrip("#").strip() for line in text.splitlines() if line.lstrip().startswith("#")), "")
41
+ value = heading or path.stem
42
+ return re.sub(r"[^a-z0-9]+", "-", value.casefold()).strip("-")
43
+
44
+
45
+ def _last_change(project: Path, relative: str) -> str | None:
46
+ try:
47
+ result = subprocess.run(
48
+ ["git", "-C", str(project), "log", "-1", "--format=%cI", "--", relative],
49
+ capture_output=True, text=True, check=False,
50
+ )
51
+ except OSError:
52
+ return None
53
+ value = result.stdout.strip()
54
+ return value or None
55
+
56
+
57
+ def audit(project: Path, docs_dir: str = "docs") -> dict:
58
+ root = project.resolve()
59
+ docs = (root / docs_dir).resolve()
60
+ if not docs.is_dir():
61
+ return {"schemaVersion": "maggie-docs-audit.v1", "passed": False, "errors": [f"docs directory does not exist: {docs}"], "files": []}
62
+ files = _files(docs)
63
+ corpus: list[tuple[Path, str]] = []
64
+ for path in _files(root):
65
+ try:
66
+ corpus.append((path, path.read_text(encoding="utf-8", errors="replace")))
67
+ except OSError:
68
+ continue
69
+ entries = []
70
+ for path in files:
71
+ try:
72
+ text = path.read_text(encoding="utf-8", errors="replace")
73
+ except OSError as error:
74
+ entries.append({"path": str(path.relative_to(root)), "status": "unreadable", "error": str(error)})
75
+ continue
76
+ relative = str(path.relative_to(root))
77
+ basename = path.name
78
+ inbound = sum(1 for source, source_text in corpus if source != path and (relative in source_text or basename in source_text))
79
+ category = _category(path)
80
+ completed_tracker = category == "plan" and bool(COMPLETED_RE.search(text))
81
+ findings: list[str] = []
82
+ if completed_tracker:
83
+ findings.append("completed-progress-tracker")
84
+ if inbound == 0 and category in {"reference", "data"}:
85
+ findings.append("unreferenced")
86
+ entries.append({
87
+ "path": relative,
88
+ "category": category,
89
+ "inboundReferences": inbound,
90
+ "lastChanged": _last_change(root, relative),
91
+ "completedTracker": completed_tracker,
92
+ "openTracker": category == "plan" and not completed_tracker,
93
+ "identity": _identity(path, text),
94
+ "findings": findings,
95
+ })
96
+ identities: dict[str, list[dict]] = {}
97
+ for item in entries:
98
+ identities.setdefault(item.get("identity", ""), []).append(item)
99
+ duplicate_groups = [group for key, group in identities.items() if key and len(group) > 1]
100
+ duplicate_paths = {item["path"] for group in duplicate_groups for item in group}
101
+ for item in entries:
102
+ item.pop("identity", None)
103
+ if item["path"] in duplicate_paths:
104
+ item["findings"].append("duplicate-candidate")
105
+ item["staleCandidate"] = bool(item["findings"])
106
+ findings = [item for item in entries if item.get("findings")]
107
+ return {
108
+ "schemaVersion": "maggie-docs-audit.v1",
109
+ "passed": True,
110
+ "project": str(root),
111
+ "docsDirectory": str(docs.relative_to(root)),
112
+ "fileCount": len(entries),
113
+ "findingCount": len(findings),
114
+ "summary": {
115
+ "reference": sum(item.get("category") == "reference" for item in entries),
116
+ "record": sum(item.get("category") == "record" for item in entries),
117
+ "plan": sum(item.get("category") == "plan" for item in entries),
118
+ "data": sum(item.get("category") == "data" for item in entries),
119
+ "completedTrackers": sum(item.get("completedTracker", False) for item in entries),
120
+ "openTrackers": sum(item.get("openTracker", False) for item in entries),
121
+ "unreferenced": sum("unreferenced" in item.get("findings", []) for item in entries),
122
+ "duplicateCandidates": len(duplicate_groups),
123
+ "staleCandidates": sum(item.get("staleCandidate", False) for item in entries),
124
+ },
125
+ "files": entries,
126
+ }
127
+
128
+
129
+ def main() -> int:
130
+ parser = argparse.ArgumentParser(description=__doc__)
131
+ parser.add_argument("audit", nargs="?", choices=("audit",), default="audit")
132
+ parser.add_argument("--project", default=".")
133
+ parser.add_argument("--docs-dir", default="docs")
134
+ parser.add_argument("--fail-on-findings", action="store_true")
135
+ parser.add_argument("--output")
136
+ args = parser.parse_args()
137
+ result = audit(Path(args.project), args.docs_dir)
138
+ if args.fail_on_findings and result.get("findingCount"):
139
+ result["passed"] = False
140
+ result.setdefault("errors", []).append("documentation findings require review")
141
+ if args.output:
142
+ output = Path(args.output).expanduser().resolve()
143
+ output.parent.mkdir(parents=True, exist_ok=True)
144
+ output.write_text(json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
145
+ result["output"] = str(output)
146
+ print(json.dumps(result, indent=2, ensure_ascii=False))
147
+ return 0 if result.get("passed") else 1
148
+
149
+
150
+ if __name__ == "__main__":
151
+ raise SystemExit(main())
@@ -4,11 +4,17 @@ from __future__ import annotations
4
4
 
5
5
  from collections.abc import Mapping
6
6
  from typing import Any
7
+ from urllib.error import HTTPError, URLError
8
+ from urllib.parse import urlparse
9
+ from urllib.request import Request, urlopen
10
+ from pathlib import Path
11
+ import json
7
12
 
8
13
 
9
14
  SCHEMA = "maggie-api-contract.v1"
10
15
  METHODS = {"get", "post", "put", "patch", "delete", "head", "options", "trace"}
11
16
  BODY_METHODS = {"post", "put", "patch"}
17
+ MAX_SPEC_BYTES = 2_000_000
12
18
 
13
19
 
14
20
  def _has_schema(value: object) -> bool:
@@ -95,3 +101,48 @@ def validate_api_contract(spec: object) -> dict[str, Any]:
95
101
  operations.append(record)
96
102
  return {"schemaVersion": SCHEMA, "passed": not errors, "operations": operations, "errors": errors,
97
103
  "operationCount": len(operations), "errorCount": len(errors)}
104
+
105
+
106
+ def _safe_url(source: str) -> tuple[str, str]:
107
+ parsed = urlparse(source)
108
+ if parsed.scheme not in {"https", "http"} or not parsed.netloc:
109
+ raise ValueError("API contract source must be a local file or an absolute HTTPS URL")
110
+ if parsed.username or parsed.password or parsed.query or parsed.fragment:
111
+ raise ValueError("API contract URL must not contain credentials, query parameters, or fragments")
112
+ host = (parsed.hostname or "").casefold()
113
+ if parsed.scheme == "http" and host not in {"localhost", "127.0.0.1", "::1"}:
114
+ raise ValueError("plain HTTP is allowed only for loopback development sources")
115
+ display = f"{parsed.scheme}://{parsed.netloc}{parsed.path or '/'}"
116
+ return source, display
117
+
118
+
119
+ def load_api_contract_source(source: str | Path, *, timeout: float = 20.0) -> tuple[object, dict[str, Any]]:
120
+ """Load a local JSON spec or fetch a safe URL without writing the response.
121
+
122
+ The caller receives source metadata suitable for an audit report. Response
123
+ bodies are parsed in memory only, so a live contract check cannot silently
124
+ create a project artifact containing a private API document.
125
+ """
126
+ value = str(source)
127
+ parsed = urlparse(value)
128
+ if parsed.scheme in {"https", "http"}:
129
+ requested, display = _safe_url(value)
130
+ request = Request(requested, headers={"Accept": "application/json", "User-Agent": "MaggieApiContract/1"})
131
+ try:
132
+ with urlopen(request, timeout=timeout) as response:
133
+ raw = response.read(MAX_SPEC_BYTES + 1)
134
+ except (HTTPError, URLError, TimeoutError) as error:
135
+ raise RuntimeError(f"could not fetch API contract URL: {error}") from error
136
+ if len(raw) > MAX_SPEC_BYTES:
137
+ raise ValueError(f"API contract response exceeds {MAX_SPEC_BYTES} bytes")
138
+ try:
139
+ document = json.loads(raw.decode("utf-8"))
140
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
141
+ raise ValueError("API contract URL did not return a UTF-8 JSON document") from error
142
+ return document, {"sourceType": "url", "source": display}
143
+
144
+ path = Path(value).expanduser().resolve()
145
+ raw = path.read_bytes()
146
+ if len(raw) > MAX_SPEC_BYTES:
147
+ raise ValueError(f"API contract file exceeds {MAX_SPEC_BYTES} bytes")
148
+ return json.loads(raw.decode("utf-8")), {"sourceType": "file", "source": str(path)}
@@ -0,0 +1,58 @@
1
+ """Validate runtime evidence that a dashboard screen actually mounted and ran."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+ from urllib.parse import urlparse
8
+
9
+
10
+ SCHEMA = "maggiedash-dashboard-runtime.v1"
11
+
12
+
13
+ def validate_runtime_evidence(evidence: object) -> dict[str, Any]:
14
+ errors: list[str] = []
15
+ if not isinstance(evidence, Mapping):
16
+ return {"schemaVersion": SCHEMA, "passed": False, "errors": ["evidence must be an object"]}
17
+ if evidence.get("schemaVersion") != SCHEMA:
18
+ errors.append(f"schemaVersion must be {SCHEMA}")
19
+ for field in ("screen", "route"):
20
+ if not isinstance(evidence.get(field), str) or not evidence[field].strip():
21
+ errors.append(f"{field} is required")
22
+ if evidence.get("mounted") is not True:
23
+ errors.append("mounted must be true")
24
+ checks = evidence.get("checks")
25
+ if not isinstance(checks, list) or not checks:
26
+ errors.append("checks must contain at least one executed check")
27
+ checks = []
28
+ check_results: list[dict[str, Any]] = []
29
+ for index, check in enumerate(checks):
30
+ if not isinstance(check, Mapping) or not isinstance(check.get("name"), str) or not check["name"].strip():
31
+ errors.append(f"checks[{index}] needs a name")
32
+ continue
33
+ if not isinstance(check.get("passed"), bool):
34
+ errors.append(f"checks[{index}].passed must be boolean")
35
+ elif check["passed"] is False:
36
+ errors.append(f"runtime check failed: {check['name']}")
37
+ check_results.append({"name": check["name"], "passed": check.get("passed")})
38
+ calls = evidence.get("dataCalls", [])
39
+ if not isinstance(calls, list):
40
+ errors.append("dataCalls must be a list")
41
+ calls = []
42
+ call_results: list[dict[str, Any]] = []
43
+ for index, call in enumerate(calls):
44
+ if not isinstance(call, Mapping):
45
+ errors.append(f"dataCalls[{index}] must be an object")
46
+ continue
47
+ url = str(call.get("url") or "")
48
+ parsed = urlparse(url)
49
+ if not url or parsed.username or parsed.password or parsed.scheme not in {"", "http", "https"}:
50
+ errors.append(f"dataCalls[{index}].url must be a relative or credential-free HTTP URL")
51
+ if not isinstance(call.get("method", "GET"), str):
52
+ errors.append(f"dataCalls[{index}].method must be a string")
53
+ if not isinstance(call.get("ok"), bool):
54
+ errors.append(f"dataCalls[{index}].ok must be boolean")
55
+ elif call["ok"] is False:
56
+ errors.append(f"data call failed: {url or 'unknown'}")
57
+ call_results.append({"url": url, "method": call.get("method", "GET"), "status": call.get("status"), "ok": call.get("ok")})
58
+ return {"schemaVersion": SCHEMA, "passed": not errors, "errors": errors, "screen": evidence.get("screen"), "route": evidence.get("route"), "mounted": evidence.get("mounted"), "checks": check_results, "dataCalls": call_results}
@@ -0,0 +1,72 @@
1
+ """Validate host-supplied table/reader evidence without connecting to a database."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+
9
+ SCHEMA = "maggie-schema-inventory.v1"
10
+
11
+
12
+ def audit_schema_inventory(inventory: object, *, fail_on_unread: bool = False) -> dict[str, Any]:
13
+ errors: list[str] = []
14
+ warnings: list[str] = []
15
+ if not isinstance(inventory, Mapping):
16
+ return {"schemaVersion": SCHEMA, "passed": False, "errors": ["inventory must be an object"], "tables": []}
17
+ if inventory.get("schemaVersion") != SCHEMA:
18
+ errors.append(f"schemaVersion must be {SCHEMA}")
19
+ tables = inventory.get("tables")
20
+ if not isinstance(tables, list):
21
+ errors.append("tables must be a list")
22
+ tables = []
23
+ seen: set[str] = set()
24
+ normalized: list[dict[str, Any]] = []
25
+ unread: list[dict[str, Any]] = []
26
+ unknown: list[str] = []
27
+ for index, table in enumerate(tables):
28
+ prefix = f"tables[{index}]"
29
+ if not isinstance(table, Mapping):
30
+ errors.append(f"{prefix} must be an object")
31
+ continue
32
+ name = str(table.get("name") or "").strip()
33
+ if not name:
34
+ errors.append(f"{prefix}.name is required")
35
+ continue
36
+ if name in seen:
37
+ errors.append(f"duplicate table: {name}")
38
+ seen.add(name)
39
+ row_count = table.get("rowCount")
40
+ if not isinstance(row_count, int) or isinstance(row_count, bool) or row_count < 0:
41
+ errors.append(f"{prefix}.rowCount must be a non-negative integer")
42
+ row_count = None
43
+ readers = table.get("readers")
44
+ if readers is None:
45
+ unknown.append(name)
46
+ warnings.append(f"{name} has no reader evidence")
47
+ reader_list: list[str] = []
48
+ reader_status = "unknown"
49
+ elif not isinstance(readers, list) or any(not isinstance(reader, str) or not reader.strip() for reader in readers):
50
+ errors.append(f"{prefix}.readers must be a list of non-empty strings")
51
+ reader_list = []
52
+ reader_status = "invalid"
53
+ else:
54
+ reader_list = sorted({reader.strip() for reader in readers})
55
+ reader_status = "read" if reader_list else "unread"
56
+ record = {"name": name, "rowCount": row_count, "readers": reader_list, "readerStatus": reader_status}
57
+ normalized.append(record)
58
+ if reader_status == "unread":
59
+ unread.append(record)
60
+ if fail_on_unread and unread:
61
+ errors.append(f"{len(unread)} declared table(s) have no reader evidence")
62
+ return {
63
+ "schemaVersion": SCHEMA,
64
+ "passed": not errors,
65
+ "errors": errors,
66
+ "warnings": warnings,
67
+ "tables": normalized,
68
+ "unreadTables": unread,
69
+ "unknownReaderEvidence": unknown,
70
+ "tableCount": len(normalized),
71
+ "unreadCount": len(unread),
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topy-ai/maggie",
3
- "version": "0.7.18",
3
+ "version": "0.7.19",
4
4
  "description": "Install and manage Maggie Skills for AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",