@topy-ai/maggie 0.7.38 → 0.7.41
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-zh-TW.md +82 -6
- package/README.md +99 -7
- package/bin/maggie.js +30 -3
- package/bundled-contracts/maggie-clone/interaction-state-v1.schema.json +26 -0
- package/bundled-contracts/maggie-content/provenance-v1.schema.json +20 -0
- package/bundled-contracts/maggie-design/brand-kit-v1.schema.json +18 -0
- package/bundled-contracts/maggie-design/browser-interactions-v1.schema.json +27 -0
- package/bundled-contracts/maggie-design/css-utility-evidence-v1.schema.json +10 -0
- package/bundled-contracts/maggie-design/dashboard-surface-v1.schema.json +11 -0
- package/bundled-contracts/maggie-design/progressive-enhancement-v1.schema.json +12 -0
- package/bundled-contracts/maggie-design/sample-surface-v1.schema.json +13 -0
- package/bundled-contracts/maggie-design/style-editing-v1.schema.json +35 -0
- package/bundled-contracts/maggie-media/image-generation-policy-v1.json +28 -0
- package/bundled-contracts/maggie-media/video-generation-policy-v1.json +40 -0
- package/bundled-contracts/maggie-media/video-job-v1.schema.json +20 -0
- package/bundled-contracts/maggie-media/video-playback-evidence-v1.schema.json +15 -0
- package/bundled-contracts/maggie-ops/npm11-preflight-v1.schema.json +17 -0
- package/bundled-contracts/maggie-scaffold/host-scaffold-v1.schema.json +25 -0
- package/bundled-contracts/maggie-seo/gsc-readiness-v1.schema.json +19 -0
- package/bundled-contracts/maggie-seo/privacy-origin-evidence-v1.schema.json +11 -0
- package/bundled-contracts/maggie-service-booking/delivery-provider-default-v1.json +8 -0
- package/bundled-contracts/maggie-service-booking/delivery-provider-v1.schema.json +16 -0
- package/bundled-contracts/maggiedash/booking-access-v1.json +5 -4
- package/bundled-contracts/maggiedash/booking-customer-surface-v1.json +12 -1
- package/bundled-contracts/maggiedash/booking-email-templates-v1.json +2 -0
- package/bundled-contracts/maggiedash/booking-host-adapter-v1.json +14 -2
- package/bundled-contracts/maggiedash/booking-runtime.v1.json +41 -0
- package/bundled-contracts/maggiedash/browser-session-v1.schema.json +18 -0
- package/bundled-contracts/maggiedash/content-overrides-v1.schema.json +19 -0
- package/bundled-contracts/maggiedash/execution-board.json +526 -26
- package/bundled-contracts/maggiedash/host-capabilities-v1.schema.json +10 -0
- package/bundled-contracts/maggiedash/public-session-cache-v1.schema.json +17 -0
- package/bundled-contracts/maggiedash/site-structure-v1.schema.json +13 -0
- package/bundled-references/browser-inspection.md +21 -0
- package/bundled-references/maggiedash-booking/ARCHITECTURE.md +218 -0
- package/bundled-references/maggiedash-booking/CURRENT-STATE.md +92 -0
- package/bundled-references/maggiedash-booking/DATA-FLOW.md +143 -0
- package/bundled-references/maggiedash-booking/DATA-MODEL.md +367 -0
- package/bundled-references/maggiedash-booking/DECISIONS.md +94 -0
- package/bundled-references/maggiedash-booking/EXECUTION-BOARD.json +2387 -0
- package/bundled-references/maggiedash-booking/HOST-ADAPTER.md +314 -0
- package/bundled-references/maggiedash-booking/ORAWELLNESS-INTEGRATION-AUDIT.md +227 -0
- package/bundled-references/maggiedash-booking/PAYMENT-GATEWAY.md +267 -0
- package/bundled-references/maggiedash-booking/PRD.md +228 -0
- package/bundled-references/maggiedash-booking/PROGRESS.md +2434 -0
- package/bundled-references/maggiedash-booking/QA-TEST-PLAN.md +235 -0
- package/bundled-references/maggiedash-booking/README.md +271 -0
- package/bundled-references/maggiedash-booking/RUNTIME-OPERATIONS.md +152 -0
- package/bundled-references/maggiedash-booking/SECURITY-COMPLIANCE.md +158 -0
- package/bundled-references/maggiedash-booking/SKILLS-AND-CLI.md +542 -0
- package/bundled-references/maggiedash-booking/STRIPE-INTEGRATION.md +129 -0
- package/bundled-references/maggiedash-booking/TASK-RUNBOOK.md +107 -0
- package/bundled-references/maggiedash-booking/TASKS.md +137 -0
- package/bundled-references/maggiedash-booking/USER-JOURNEYS.md +224 -0
- package/bundled-references/maggiedash-booking/diagrams/booking-dfd.excalidraw +1 -0
- package/bundled-references/maggiedash-booking/diagrams/booking-dfd.mmd +16 -0
- package/bundled-references/maggiedash-booking/diagrams/booking-dfd.png +0 -0
- package/bundled-references/maggiedash-booking/diagrams/booking-dfd.svg +1 -0
- package/bundled-references/maggiedash-booking/diagrams/booking-state-machine.mmd +19 -0
- package/bundled-references/maggiedash-booking/diagrams/booking-state-machine.png +0 -0
- package/bundled-references/maggiedash-booking/diagrams/booking-state-machine.svg +1 -0
- package/bundled-references/maggiedash-booking/diagrams/manager-journey.excalidraw +1 -0
- package/bundled-references/maggiedash-booking/diagrams/manager-journey.mmd +11 -0
- package/bundled-references/maggiedash-booking/diagrams/manager-journey.png +0 -0
- package/bundled-references/maggiedash-booking/diagrams/manager-journey.svg +1 -0
- package/bundled-references/maggiedash-booking/diagrams/payment-sequence.mmd +20 -0
- package/bundled-references/maggiedash-booking/diagrams/payment-sequence.png +0 -0
- package/bundled-references/maggiedash-booking/diagrams/payment-sequence.svg +1 -0
- package/bundled-references/maggiedash-booking/diagrams/system-context.excalidraw +1 -0
- package/bundled-references/maggiedash-booking/diagrams/system-context.mmd +10 -0
- package/bundled-references/maggiedash-booking/diagrams/system-context.png +0 -0
- package/bundled-references/maggiedash-booking/diagrams/system-context.svg +1 -0
- package/bundled-skills/maggie-blog/SKILL.md +12 -0
- package/bundled-skills/maggie-blog-bootstrap/SKILL.md +39 -0
- package/bundled-skills/maggie-booking/SKILL.md +112 -18
- package/bundled-skills/maggie-clone/SKILL.md +13 -0
- package/bundled-skills/maggie-deployment/SKILL.md +6 -0
- package/bundled-skills/maggie-design/SKILL.md +64 -3
- package/bundled-skills/maggie-ops/SKILL.md +12 -0
- package/bundled-skills/maggie-seo-geo/SKILL.md +45 -0
- package/bundled-skills/maggie-service-booking/SKILL.md +14 -0
- package/bundled-tools/clis/maggie_analytics.py +43 -1
- package/bundled-tools/clis/maggie_booking.py +161 -26
- package/bundled-tools/clis/maggie_browser_audit.py +99 -3
- package/bundled-tools/clis/maggie_clone.py +46 -1
- package/bundled-tools/clis/maggie_contracts.py +393 -0
- package/bundled-tools/clis/maggie_dash.py +22 -6
- package/bundled-tools/clis/maggie_design.py +84 -12
- package/bundled-tools/clis/maggie_service_booking.py +50 -1
- package/bundled-tools/clis/maggie_workflows.py +387 -0
- package/bundled-tools/clis/site_audit.py +77 -1
- package/bundled-tools/integrations/analytics.md +14 -0
- package/bundled-tools/runtime/site_baseline.py +3 -0
- package/package.json +1 -1
- package/references/browser-inspection.md +21 -0
|
@@ -8,6 +8,7 @@ import hashlib
|
|
|
8
8
|
import json
|
|
9
9
|
import re
|
|
10
10
|
import sys
|
|
11
|
+
from datetime import datetime, timedelta, timezone
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from urllib.parse import urljoin, urlparse, urlunparse
|
|
13
14
|
from html.parser import HTMLParser
|
|
@@ -69,9 +70,20 @@ class PageParser(HTMLParser):
|
|
|
69
70
|
self.paragraphs = []
|
|
70
71
|
self.links = []
|
|
71
72
|
self.fragment_ids = set()
|
|
73
|
+
self.primary_navigation_links = []
|
|
74
|
+
self._header_depth = 0
|
|
75
|
+
self._nav_depth = 0
|
|
76
|
+
self._primary_nav_depth = 0
|
|
72
77
|
|
|
73
78
|
def handle_starttag(self, tag, attrs):
|
|
74
79
|
data = dict(attrs)
|
|
80
|
+
if tag == "header":
|
|
81
|
+
self._header_depth += 1
|
|
82
|
+
if tag == "nav":
|
|
83
|
+
self._nav_depth += 1
|
|
84
|
+
label = f"{data.get('aria-label', '')} {data.get('id', '')} {data.get('class', '')}".lower()
|
|
85
|
+
if self._header_depth or any(token in label for token in ("primary", "main", "navigation")):
|
|
86
|
+
self._primary_nav_depth += 1
|
|
75
87
|
if data.get("id"):
|
|
76
88
|
self.fragment_ids.add(data["id"])
|
|
77
89
|
if tag == "a" and data.get("name"):
|
|
@@ -106,6 +118,8 @@ class PageParser(HTMLParser):
|
|
|
106
118
|
self._jsonld = []
|
|
107
119
|
elif tag == "a" and data.get("href"):
|
|
108
120
|
self.anchors += 1
|
|
121
|
+
if self._primary_nav_depth:
|
|
122
|
+
self.primary_navigation_links.append(data.get("href", ""))
|
|
109
123
|
elif tag == "img":
|
|
110
124
|
self.images.append({"src": data.get("src", ""), "alt": data.get("alt")})
|
|
111
125
|
|
|
@@ -132,6 +146,12 @@ class PageParser(HTMLParser):
|
|
|
132
146
|
except json.JSONDecodeError:
|
|
133
147
|
self.jsonld_values.append(None)
|
|
134
148
|
self._jsonld = None
|
|
149
|
+
if tag == "nav":
|
|
150
|
+
self._nav_depth = max(0, self._nav_depth - 1)
|
|
151
|
+
if self._primary_nav_depth:
|
|
152
|
+
self._primary_nav_depth -= 1
|
|
153
|
+
elif tag == "header":
|
|
154
|
+
self._header_depth = max(0, self._header_depth - 1)
|
|
135
155
|
|
|
136
156
|
def handle_data(self, data):
|
|
137
157
|
if not self.hidden_depth and data.strip():
|
|
@@ -157,6 +177,20 @@ def fetch(url: str, evidence: dict | None = None) -> tuple[int, str, str]:
|
|
|
157
177
|
return response.status, response.headers.get_content_type(), body
|
|
158
178
|
|
|
159
179
|
|
|
180
|
+
def indexability_if_indexed(page: PageParser, robots: list[str]) -> dict:
|
|
181
|
+
"""Explain what a noindex page would fail if it became indexable."""
|
|
182
|
+
tokens = [token.strip().rsplit(":", 1)[-1] for directive in robots for token in directive.split(",")]
|
|
183
|
+
blocked = sorted(set(tokens) & {"noindex", "none"})
|
|
184
|
+
checks = {
|
|
185
|
+
"title": bool(page.title),
|
|
186
|
+
"description": bool(page.meta.get("description")),
|
|
187
|
+
"canonical": bool(page.canonical) and urlparse(page.canonical).fragment == "",
|
|
188
|
+
"headings": bool(page.headings) and page.h1 == 1,
|
|
189
|
+
"jsonld": page.jsonld > 0 and all(item is not None for item in page.jsonld_values),
|
|
190
|
+
}
|
|
191
|
+
return {"applicable": bool(blocked), "blockedBy": blocked, "wouldPass": all(checks.values()) if blocked else None, "checks": checks if blocked else {}}
|
|
192
|
+
|
|
193
|
+
|
|
160
194
|
def audit_page(url: str, html: str, status: int, content_type: str, expected_languages: set[str] | None = None, response_evidence: dict | None = None) -> dict:
|
|
161
195
|
page = PageParser()
|
|
162
196
|
page.feed(html)
|
|
@@ -165,6 +199,7 @@ def audit_page(url: str, html: str, status: int, content_type: str, expected_lan
|
|
|
165
199
|
robots = page.robots_directives + [value.lower() for value in response_evidence["xRobotsTag"]]
|
|
166
200
|
robots_tokens = [token.strip() for directive in robots for token in directive.split(",")]
|
|
167
201
|
robots_tokens = [token.rsplit(":", 1)[-1].strip() for token in robots_tokens]
|
|
202
|
+
hypothetical = indexability_if_indexed(page, robots)
|
|
168
203
|
return {
|
|
169
204
|
"url": url,
|
|
170
205
|
"status": status,
|
|
@@ -193,6 +228,7 @@ def audit_page(url: str, html: str, status: int, content_type: str, expected_lan
|
|
|
193
228
|
"lang": page.lang, "hreflang": page.hreflang, "robots": robots,
|
|
194
229
|
"jsonld": page.jsonld_values, "images": page.images,
|
|
195
230
|
"fragmentIds": sorted(page.fragment_ids),
|
|
231
|
+
"primaryNavigationLinks": page.primary_navigation_links,
|
|
196
232
|
"structureHash": hashlib.sha256(json.dumps(stable_structure(page.structure), sort_keys=True).encode()).hexdigest(),
|
|
197
233
|
"textHash": hashlib.sha256(" ".join(page.visible_text).encode()).hexdigest(),
|
|
198
234
|
},
|
|
@@ -215,9 +251,38 @@ def audit_page(url: str, html: str, status: int, content_type: str, expected_lan
|
|
|
215
251
|
"robots": not robots or not any(token in {"noindex", "none", "nofollow"} for token in robots_tokens),
|
|
216
252
|
"robots_directives": robots,
|
|
217
253
|
"robots_conflict": len({token for token in robots_tokens if token in {"index", "noindex", "follow", "nofollow", "none"}} & {"index", "noindex"}) > 1 or len({token for token in robots_tokens if token in {"follow", "nofollow", "none"}} & {"follow", "nofollow"}) > 1,
|
|
254
|
+
"primary_navigation_links": page.primary_navigation_links,
|
|
255
|
+
"indexabilityIfIndexed": hypothetical,
|
|
218
256
|
}
|
|
219
257
|
|
|
220
258
|
|
|
259
|
+
def primary_navigation_check(base: str, page: PageParser, fetcher=fetch) -> dict:
|
|
260
|
+
"""Ensure links presented as primary navigation are indexable destinations."""
|
|
261
|
+
origin = urlparse(base).netloc
|
|
262
|
+
routes = []
|
|
263
|
+
for href in page.primary_navigation_links:
|
|
264
|
+
target = urljoin(base + "/", href)
|
|
265
|
+
parsed = urlparse(target)
|
|
266
|
+
if parsed.scheme not in {"http", "https"} or parsed.netloc != origin:
|
|
267
|
+
continue
|
|
268
|
+
target = urlunparse((parsed.scheme, parsed.netloc, parsed.path or "/", "", "", ""))
|
|
269
|
+
if target not in routes:
|
|
270
|
+
routes.append(target)
|
|
271
|
+
violations = []
|
|
272
|
+
for target in routes:
|
|
273
|
+
try:
|
|
274
|
+
status, content_type, body = fetcher(target)
|
|
275
|
+
destination = PageParser(); destination.feed(body)
|
|
276
|
+
directives = [item.lower() for item in destination.robots_directives]
|
|
277
|
+
tokens = [token.strip().rsplit(":", 1)[-1] for directive in directives for token in directive.split(",")]
|
|
278
|
+
blocked = sorted(set(tokens) & {"noindex", "nofollow", "none"})
|
|
279
|
+
if blocked:
|
|
280
|
+
violations.append({"url": target, "status": status, "directives": directives, "blocked": blocked})
|
|
281
|
+
except Exception as exc:
|
|
282
|
+
violations.append({"url": target, "error": type(exc).__name__})
|
|
283
|
+
return {"ok": not violations, "links": routes, "violations": violations}
|
|
284
|
+
|
|
285
|
+
|
|
221
286
|
def summarize_crawl(pages: list[dict]) -> dict:
|
|
222
287
|
"""Count captured evidence, preserving regional/script locale distinctions."""
|
|
223
288
|
groups = {}
|
|
@@ -400,6 +465,12 @@ def parse_baseline_reasons(values: list[str], drift_urls: set[str], reason_all:
|
|
|
400
465
|
return reasons
|
|
401
466
|
|
|
402
467
|
|
|
468
|
+
def baseline_ack_expiry(days: int) -> str:
|
|
469
|
+
if not 1 <= days <= 3650:
|
|
470
|
+
raise ValueError("baseline acknowledgement TTL must be between 1 and 3650 days")
|
|
471
|
+
return (datetime.now(timezone.utc) + timedelta(days=days)).isoformat().replace("+00:00", "Z")
|
|
472
|
+
|
|
473
|
+
|
|
403
474
|
def main() -> int:
|
|
404
475
|
parser = argparse.ArgumentParser()
|
|
405
476
|
parser.add_argument("url")
|
|
@@ -423,6 +494,7 @@ def main() -> int:
|
|
|
423
494
|
parser.add_argument("--baseline-id", help="required stable ID for --recapture-baseline")
|
|
424
495
|
parser.add_argument("--reason", action="append", default=[], metavar="URL=REASON", help="reviewer-approved reason for one drifting URL; repeat for every drift")
|
|
425
496
|
parser.add_argument("--reason-all", help="reviewer-approved reason applied to every drifting URL during baseline recapture")
|
|
497
|
+
parser.add_argument("--reason-ttl-days", type=int, default=30, help="expiry for a reviewed baseline drift acknowledgement (default: 30)")
|
|
426
498
|
parser.add_argument("--reviewer", help="required for --save-baseline")
|
|
427
499
|
args = parser.parse_args()
|
|
428
500
|
if args.max_pages < 1:
|
|
@@ -455,7 +527,8 @@ def main() -> int:
|
|
|
455
527
|
base = args.url.rstrip("/")
|
|
456
528
|
checks = {}
|
|
457
529
|
try:
|
|
458
|
-
|
|
530
|
+
homepage_response = {}
|
|
531
|
+
status, content_type, html = fetch(base, homepage_response)
|
|
459
532
|
page = PageParser()
|
|
460
533
|
page.feed(html)
|
|
461
534
|
checks["homepage"] = {"ok": status == 200 and content_type == "text/html", "status": status, "content_type": content_type}
|
|
@@ -470,9 +543,11 @@ def main() -> int:
|
|
|
470
543
|
checks["jsonld"] = {"ok": page.jsonld > 0 and all(item is not None for item in page.jsonld_values), "count": page.jsonld}
|
|
471
544
|
checks["entity_jsonld"] = {"ok": any(isinstance(item, dict) and item.get("@type") and (item.get("url") or item.get("@id")) for item in page.jsonld_values), "count": page.jsonld}
|
|
472
545
|
checks["crawlable_links"] = {"ok": page.anchors > 0, "count": page.anchors}
|
|
546
|
+
checks["primary_navigation_indexability"] = primary_navigation_check(base, page)
|
|
473
547
|
robots_tokens = [token.strip() for directive in page.robots_directives for token in directive.split(",")]
|
|
474
548
|
checks["robots_directive"] = {"ok": not page.robots_directives or not any(token in {"noindex", "none", "nofollow"} for token in robots_tokens), "directives": page.robots_directives}
|
|
475
549
|
checks["robots_conflict"] = {"ok": not (len({token for token in robots_tokens if token in {"index", "noindex"}}) > 1 or len({token for token in robots_tokens if token in {"follow", "nofollow"}}) > 1), "directives": page.robots_directives}
|
|
550
|
+
checks["indexability_if_indexed"] = {"ok": True, **indexability_if_indexed(page, page.robots_directives + [value.lower() for value in homepage_response.get("xRobotsTag", [])])}
|
|
476
551
|
if args.check_hreflang or args.check_translation_completeness:
|
|
477
552
|
checks["hreflang"] = hreflang_check(base, page, expected_languages) if args.check_hreflang else {"ok": True, "links": page.hreflang}
|
|
478
553
|
if args.check_translation_completeness and expected_languages:
|
|
@@ -565,6 +640,7 @@ def main() -> int:
|
|
|
565
640
|
baseline_id=args.baseline_id,
|
|
566
641
|
supersedes=previous_baseline.get("baselineId") or site_baseline.baseline_fingerprint(previous_baseline),
|
|
567
642
|
change_reasons=reasons,
|
|
643
|
+
approval_expires_at=baseline_ack_expiry(args.reason_ttl_days),
|
|
568
644
|
)
|
|
569
645
|
site_baseline.save(args.recapture_baseline, recaptured)
|
|
570
646
|
result["baselineRecapture"] = {
|
|
@@ -60,6 +60,20 @@ GSC data is read-only in this starter. Any query, page, or indexing report must
|
|
|
60
60
|
show its date window and property so an agent does not confuse an empty result
|
|
61
61
|
with a failed connection.
|
|
62
62
|
|
|
63
|
+
The GSC readiness evidence contract is explicit and can be checked before the
|
|
64
|
+
combined release gate:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
maggie analytics gsc-readiness \
|
|
68
|
+
--gsc-evidence .maggie/gsc-readiness.json
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The evidence must cover property/verification, canonical origin, robots,
|
|
72
|
+
sitemap, read-only authorization, query/readback, and production smoke. Use
|
|
73
|
+
`--gsc-evidence ... --require-gsc` with `maggie analytics release-gate` to make
|
|
74
|
+
those checks blocking. The command does not authorize a property or mutate
|
|
75
|
+
Search Console.
|
|
76
|
+
|
|
63
77
|
Run the deterministic configuration gate before enabling production tracking:
|
|
64
78
|
|
|
65
79
|
```bash
|
|
@@ -21,6 +21,7 @@ def snapshot(
|
|
|
21
21
|
baseline_id: str | None = None,
|
|
22
22
|
supersedes: str | None = None,
|
|
23
23
|
change_reasons: dict[str, str] | None = None,
|
|
24
|
+
approval_expires_at: str | None = None,
|
|
24
25
|
) -> dict:
|
|
25
26
|
if not reviewer.strip():
|
|
26
27
|
raise ValueError("baseline requires a reviewer")
|
|
@@ -58,6 +59,8 @@ def snapshot(
|
|
|
58
59
|
result["supersedes"] = supersedes
|
|
59
60
|
if change_reasons:
|
|
60
61
|
result["approvedChangeReasons"] = {key: change_reasons[key] for key in sorted(change_reasons)}
|
|
62
|
+
if approval_expires_at:
|
|
63
|
+
result["approval"] = {"type": "reviewed-drift-acknowledgement", "expiresAt": approval_expires_at}
|
|
61
64
|
return result
|
|
62
65
|
|
|
63
66
|
|
package/package.json
CHANGED
|
@@ -27,6 +27,27 @@ interaction states and triggers
|
|
|
27
27
|
responsive differences
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
Behavioral checks may be declared instead of hidden in a bespoke script. The
|
|
31
|
+
manifest is safe to commit because input values are not copied into evidence:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"schemaVersion": "maggie-browser-interactions.v1",
|
|
36
|
+
"steps": [
|
|
37
|
+
{"id": "open", "action": "click", "selector": "[data-menu]"},
|
|
38
|
+
{"id": "visible", "action": "assert-visible", "selector": "[data-drawer]"},
|
|
39
|
+
{"id": "styles", "action": "assert-style", "selector": "[data-drawer]", "property": "display", "value": "block"},
|
|
40
|
+
{"id": "privacy", "action": "assert-no-request", "origin": "third-party.example"}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Run it with `maggie browser-audit ... --interactions .maggie/interactions.json`.
|
|
46
|
+
Supported actions cover click, fill, type, select, press, wait, visibility and
|
|
47
|
+
state assertions, computed-style assertions, and new-request assertions. The
|
|
48
|
+
browser adapter owns the session and the audit records only step IDs and
|
|
49
|
+
redacted outcomes.
|
|
50
|
+
|
|
30
51
|
If a target requires login, a bot challenge, a consent interaction, or a
|
|
31
52
|
private browser profile, stop at that boundary and ask the user to provide
|
|
32
53
|
authorized access. Do not bypass access controls or record cookies in project
|