From 916d30f63ea358f1bcbee7b9d37b36cf8dbb9675 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Tue, 12 May 2026 14:05:07 +0200 Subject: [PATCH] =?UTF-8?q?chore(voyage):=20release=20v5.0.0=20=E2=80=94?= =?UTF-8?q?=20remove=20bespoke=20playground=20+=20/trekrevise=20+=20Handov?= =?UTF-8?q?er=208;=20render=20produced=20artifacts=20to=20HTML=20+=20link,?= =?UTF-8?q?=20annotate=20via=20/playground?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v4.2/v4.3 bespoke playground SPA (~388 KB), the /trekrevise command, Handover 8 (annotation → revision), the supporting lib/ modules (anchor-parser, annotation-digest, markdown-write, revision-guard), the Playwright e2e suite, and the @playwright/test / @axe-core/playwright devDeps are removed. A browser walkthrough found the playground borderline unusable, and it duplicated the official /playground plugin's document-critique / diff-review templates. In their place: scripts/render-artifact.mjs — a small, zero-dependency renderer that turns a brief/plan/review .md into a self-contained, design-system-styled, zero-network .html (frontmatter folded into a
block). /trekbrief, /trekplan, and /trekreview call it on their last step and print the file:// link; to annotate, run /playground (document-critique) on the .md and paste the generated prompt back. Resolves the v4.3.1-deferred findings as moot (their target files are deleted). npm test green: 509 tests, 507 pass, 0 fail, 2 skipped. Co-Authored-By: Claude Opus 4.7 --- .claude-plugin/marketplace.json | 2 +- CLAUDE.md | 2 +- README.md | 21 +- plugins/voyage/.claude-plugin/plugin.json | 4 +- plugins/voyage/.gitignore | 6 + plugins/voyage/CHANGELOG.md | 63 +- plugins/voyage/CLAUDE.md | 22 +- plugins/voyage/README.md | 90 +- .../voyage/agents/planning-orchestrator.md | 11 - plugins/voyage/commands/trekbrief.md | 17 + plugins/voyage/commands/trekplan.md | 64 +- plugins/voyage/commands/trekreview.md | 16 +- plugins/voyage/commands/trekrevise.md | 508 --- plugins/voyage/docs/HANDOVER-CONTRACTS.md | 95 +- plugins/voyage/docs/annotation-quickstart.md | 55 - .../voyage/docs/sc1-checklist-verification.md | 129 - plugins/voyage/docs/screenshots/README.md | 89 - plugins/voyage/lib/parsers/anchor-parser.mjs | 241 -- .../voyage/lib/parsers/annotation-digest.mjs | 49 - plugins/voyage/lib/util/markdown-write.mjs | 129 - plugins/voyage/lib/util/revision-guard.mjs | 110 - .../voyage/lib/validators/brief-validator.mjs | 8 +- .../voyage/lib/validators/plan-validator.mjs | 8 +- .../lib/validators/review-validator.mjs | 8 +- plugins/voyage/package-lock.json | 94 +- plugins/voyage/package.json | 10 +- plugins/voyage/playground/README.md | 136 - .../playground/lib/VENDOR-MANIFEST.json | 23 - .../voyage/playground/lib/dompurify.min.js | 3 - .../voyage/playground/lib/highlight.min.js | 25 - .../lib/markdown-it-front-matter.min.js | 152 - .../voyage/playground/lib/markdown-it.min.js | 2 - .../playground-design-system/CHANGELOG.md | 98 - .../playground-design-system/MANIFEST.json | 36 - .../vendor/playground-design-system/README.md | 234 -- .../vendor/playground-design-system/base.css | 265 -- .../components-tier2.css | 352 -- .../components-tier3-supplement.css | 1455 -------- .../components-tier3.css | 717 ---- .../playground-design-system/components.css | 659 ---- .../vendor/playground-design-system/fonts.css | 84 - .../fonts/Inter-Bold.woff2 | Bin 111040 -> 0 bytes .../fonts/Inter-Medium.woff2 | Bin 111380 -> 0 bytes .../fonts/Inter-Regular.woff2 | Bin 108488 -> 0 bytes .../fonts/Inter-SemiBold.woff2 | Bin 111588 -> 0 bytes .../fonts/JetBrainsMono-Medium.woff2 | Bin 93824 -> 0 bytes .../fonts/JetBrainsMono-Regular.woff2 | Bin 92164 -> 0 bytes .../fonts/JetBrainsMono-SemiBold.woff2 | Bin 94472 -> 0 bytes .../fonts/LICENSE-Inter.txt | 92 - .../fonts/LICENSE-JetBrainsMono.txt | 93 - .../fonts/LICENSE-SourceSerif4.md | 93 - .../fonts/LICENSES.md | 42 - .../fonts/SourceSerif4-Regular.woff2 | Bin 107360 -> 0 bytes .../fonts/SourceSerif4-Semibold.woff2 | Bin 112520 -> 0 bytes .../vendor/playground-design-system/print.css | 176 - .../schemas/finding.schema.json | 88 - .../schemas/okr-set.schema.json | 78 - .../schemas/ros-threat.schema.json | 59 - .../playground-design-system/tokens.css | 235 -- .../voyage/playground/voyage-playground.html | 3092 ----------------- plugins/voyage/playwright.config.mjs | 23 - plugins/voyage/scripts/render-artifact.mjs | 441 ++- .../voyage/scripts/vendor-playground-libs.mjs | 174 - plugins/voyage/settings.json | 7 - plugins/voyage/templates/plan-template.md | 16 - .../voyage/templates/trekbrief-template.md | 16 - .../voyage/templates/trekreview-template.md | 16 - .../e2e/snapshots/voyage-playground-dark.png | Bin 94381 -> 0 bytes .../e2e/snapshots/voyage-playground-light.png | Bin 95365 -> 0 bytes .../tests/e2e/voyage-playground-a11y.spec.mjs | 143 - .../e2e/voyage-playground-network.spec.mjs | 33 - .../fixtures/annotation/annotation-brief.md | 34 - .../fixtures/annotation/annotation-example.md | 27 - .../annotation/annotation-plan-large.md | 1090 ------ .../fixtures/annotation/annotation-plan.md | 64 - .../fixtures/annotation/annotation-review.md | 32 - .../playground/v43-export-bundle.json | 25 - .../playground/v43-plan-pre-annotate.md | 69 - .../fixtures/screenshot-project/brief.md | 11 - .../docs/screenshots/dashboard/sample.png | Bin 70 -> 0 bytes .../annotation-block-boundary.test.mjs | 168 - .../annotation-export-schema.test.mjs | 291 -- .../integration/annotation-roundtrip.test.mjs | 133 - .../integration/schema-rollback.test.mjs | 135 - .../voyage/tests/lib/doc-consistency.test.mjs | 302 +- .../voyage/tests/lib/markdown-write.test.mjs | 189 - .../voyage/tests/lib/revision-guard.test.mjs | 135 - .../tests/lib/source-annotations.test.mjs | 244 -- .../tests/parsers/anchor-parser.test.mjs | 130 - .../tests/parsers/annotation-digest.test.mjs | 63 - .../voyage-playground-structure.test.mjs | 88 - .../playground/voyage-playground.test.mjs | 710 ---- .../tests/scripts/render-artifact.test.mjs | 176 +- ...brief-validator-annotation-fields.test.mjs | 87 - .../plan-validator-annotation-fields.test.mjs | 79 - ...eview-validator-annotation-fields.test.mjs | 89 - 96 files changed, 642 insertions(+), 14738 deletions(-) delete mode 100644 plugins/voyage/commands/trekrevise.md delete mode 100644 plugins/voyage/docs/annotation-quickstart.md delete mode 100644 plugins/voyage/docs/sc1-checklist-verification.md delete mode 100644 plugins/voyage/docs/screenshots/README.md delete mode 100644 plugins/voyage/lib/parsers/anchor-parser.mjs delete mode 100644 plugins/voyage/lib/parsers/annotation-digest.mjs delete mode 100644 plugins/voyage/lib/util/markdown-write.mjs delete mode 100644 plugins/voyage/lib/util/revision-guard.mjs delete mode 100644 plugins/voyage/playground/README.md delete mode 100644 plugins/voyage/playground/lib/VENDOR-MANIFEST.json delete mode 100644 plugins/voyage/playground/lib/dompurify.min.js delete mode 100644 plugins/voyage/playground/lib/highlight.min.js delete mode 100644 plugins/voyage/playground/lib/markdown-it-front-matter.min.js delete mode 100644 plugins/voyage/playground/lib/markdown-it.min.js delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/CHANGELOG.md delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/MANIFEST.json delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/README.md delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/base.css delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/components-tier2.css delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/components-tier3-supplement.css delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/components-tier3.css delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/components.css delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/fonts.css delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/fonts/Inter-Bold.woff2 delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/fonts/Inter-Medium.woff2 delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/fonts/Inter-Regular.woff2 delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/fonts/Inter-SemiBold.woff2 delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/fonts/JetBrainsMono-Medium.woff2 delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/fonts/JetBrainsMono-Regular.woff2 delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/fonts/JetBrainsMono-SemiBold.woff2 delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/fonts/LICENSE-Inter.txt delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/fonts/LICENSE-JetBrainsMono.txt delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/fonts/LICENSE-SourceSerif4.md delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/fonts/LICENSES.md delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/fonts/SourceSerif4-Regular.woff2 delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/fonts/SourceSerif4-Semibold.woff2 delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/print.css delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/schemas/finding.schema.json delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/schemas/okr-set.schema.json delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/schemas/ros-threat.schema.json delete mode 100644 plugins/voyage/playground/vendor/playground-design-system/tokens.css delete mode 100644 plugins/voyage/playground/voyage-playground.html delete mode 100644 plugins/voyage/playwright.config.mjs delete mode 100644 plugins/voyage/scripts/vendor-playground-libs.mjs delete mode 100644 plugins/voyage/tests/e2e/snapshots/voyage-playground-dark.png delete mode 100644 plugins/voyage/tests/e2e/snapshots/voyage-playground-light.png delete mode 100644 plugins/voyage/tests/e2e/voyage-playground-a11y.spec.mjs delete mode 100644 plugins/voyage/tests/e2e/voyage-playground-network.spec.mjs delete mode 100644 plugins/voyage/tests/fixtures/annotation/annotation-brief.md delete mode 100644 plugins/voyage/tests/fixtures/annotation/annotation-example.md delete mode 100644 plugins/voyage/tests/fixtures/annotation/annotation-plan-large.md delete mode 100644 plugins/voyage/tests/fixtures/annotation/annotation-plan.md delete mode 100644 plugins/voyage/tests/fixtures/annotation/annotation-review.md delete mode 100644 plugins/voyage/tests/fixtures/playground/v43-export-bundle.json delete mode 100644 plugins/voyage/tests/fixtures/playground/v43-plan-pre-annotate.md delete mode 100644 plugins/voyage/tests/fixtures/screenshot-project/brief.md delete mode 100644 plugins/voyage/tests/fixtures/screenshot-project/docs/screenshots/dashboard/sample.png delete mode 100644 plugins/voyage/tests/integration/annotation-block-boundary.test.mjs delete mode 100644 plugins/voyage/tests/integration/annotation-export-schema.test.mjs delete mode 100644 plugins/voyage/tests/integration/annotation-roundtrip.test.mjs delete mode 100644 plugins/voyage/tests/integration/schema-rollback.test.mjs delete mode 100644 plugins/voyage/tests/lib/markdown-write.test.mjs delete mode 100644 plugins/voyage/tests/lib/revision-guard.test.mjs delete mode 100644 plugins/voyage/tests/lib/source-annotations.test.mjs delete mode 100644 plugins/voyage/tests/parsers/anchor-parser.test.mjs delete mode 100644 plugins/voyage/tests/parsers/annotation-digest.test.mjs delete mode 100644 plugins/voyage/tests/playground/voyage-playground-structure.test.mjs delete mode 100644 plugins/voyage/tests/playground/voyage-playground.test.mjs delete mode 100644 plugins/voyage/tests/validators/brief-validator-annotation-fields.test.mjs delete mode 100644 plugins/voyage/tests/validators/plan-validator-annotation-fields.test.mjs delete mode 100644 plugins/voyage/tests/validators/review-validator-annotation-fields.test.mjs diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 3fe6a5c..d87c54d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -23,7 +23,7 @@ { "name": "voyage", "source": "./plugins/voyage", - "description": "Voyage — brief, research, plan, execute, review, revise, continue. Contract-driven Claude Code pipeline with specialized agent swarms, external research triangulation, adversarial review, post-hoc independent review with Handover 6 feedback loop, operator-driven artifact annotation (Handover 8) via a dashboard-centric marketplace playground, multi-session resumption, session decomposition, and headless execution." + "description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline with specialized agent swarms, external research triangulation, adversarial review, post-hoc independent review with Handover 6 feedback loop, multi-session resumption, session decomposition, and headless execution. Renders produced artifacts to self-contained HTML + link; annotation via the official /playground plugin." }, { "name": "linkedin-thought-leadership", diff --git a/CLAUDE.md b/CLAUDE.md index b604e94..d79deb4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,7 @@ plugins/ llm-security/ v6.0.0 — Security scanning, auditing, threat modeling ms-ai-architect/ v1.13.1 — Microsoft AI architecture (Cosmo Skyberg persona) + manual KB-refresh slash command okr/ v1.0.0 — OKR guidance for Norwegian public sector - voyage/ v4.3.0 — Brief, research, plan, execute, review, revise, continue. Contract-driven Claude Code pipeline (seven-command universal pipeline + multi-session resumption + --gates autonomy chain + Handover 8 annotation pipeline + dashboard-centric marketplace playground). v4.3.0 ships with 3 known re-review findings deferred to v4.3.1 (defense-in-depth + conformance; ready Wave-4 plan exists). + voyage/ v5.0.0 — Brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline (six-command universal pipeline + multi-session resumption + --gates autonomy chain). Renders produced artifacts to self-contained HTML + link; annotation via the official /playground plugin. v5.0.0 removed the v4.2/v4.3 bespoke playground + /trekrevise + Handover 8 (NIH; duplicated /playground's document-critique). shared/ playground-design-system/ v0.1 — Aksel/Digdir-aligned CSS design system + JSON schemas + self-hosted Inter/JetBrains Mono/Source Serif 4 fonts (Tier 1+2+3 wave 1+wave 2 = 20 Tier 3 components total). Consumed by ms-ai-architect, okr, llm-security, voyage, config-audit diff --git a/README.md b/README.md index b0bb57f..9438d86 100644 --- a/README.md +++ b/README.md @@ -77,27 +77,26 @@ Key commands: `/config-audit posture`, `/config-audit feature-gap`, `/config-aud --- -### [Voyage](plugins/voyage/) `v4.3.0` +### [Voyage](plugins/voyage/) `v5.0.0` -Deep requirements gathering, research, implementation planning, self-verifying execution, independent post-hoc review, operator-driven artifact annotation via voyage's marketplace playground, and zero-friction multi-session resumption — with specialized agent swarms, adversarial review, and failure recovery. Seven-command (brief, research, plan, execute, review, **revise**, continue) universal pipeline. +Deep requirements gathering, research, implementation planning, self-verifying execution, independent post-hoc review, and zero-friction multi-session resumption — with specialized agent swarms, adversarial review, and failure recovery. Six-command (brief, research, plan, execute, review, continue) universal pipeline. `/trekbrief`, `/trekplan`, and `/trekreview` render their artifact to a self-contained HTML view and print the `file://` link; annotation is delegated to the official `/playground` plugin. -v4.3.0 (non-breaking) rebuilds the v4.2 playground with a dashboard-centric layout, file://-loader, and matured anchor-rendering. `playground/voyage-playground.html` now opens a `.claude/projects//`-mappe directly via `webkitdirectory` directory-picker, drag-drop with `webkitGetAsEntry` recursive walk, or `?project=/abs/path` URL-parameter — no more paste-into-textarea workflow. The new `fleet-grid` of `fleet-tile` per artifact with drill-down detail surface achieves visual parity with `plugins/llm-security/playground/llm-security-playground.html`. Anchor-rendering matures: block-boundary placement with code-fence/table/list-item fallback, browser-side `parseAnchor` mirroring Node-side regex, numbered-badge gutter + yellow-tint highlight, hidden-by-default sidebar-rail with J/K keyboard navigation, two-opacity pattern (active 100% / inactive 40% / resolved 30% strikethrough). A11Y panel built from DS-primitives. Screenshots-spor convention via `window.__voyage` hooks (`navigate` / `scheduleRender` / `getProjectArtifacts`) + `docs/screenshots/`. Path-traversal + symlink/dotfile filter (`isProjectPathSafe`). DOMPurify ≥ 3.1.1 vendored for `sanitizeAnnotation`; HTML-comment indirect prompt-injection mitigation via `parseAnchor`-allowlist gate before `md.render`. Total bundle 388 KB / 460 KB HALT-gate. Test pyramid Groups A-D — Group A 17 static-HTML grep tests (SC1 10-element checklist + SC3 + SC6 + SC7 tag-level no-CDN), Group B 9 structure tests, Group C 7 export-bundle schema + `annotation_digest` SHA-256 validity tests, Group D 5 Playwright e2e specs (light/dark axe-core absolute zero-violation + pixel-diff smoke + inline-gallery + zero-external-network authoritative gate via `npm run test:e2e`). Test count: 672 → 711 pass / 0 fail / 2 skipped (713 total). A Sesjon 13–18 independent-review remediation cycle then closed all 11 review findings (DOMPurify on the artifact body, inline screenshots gallery, absolute-zero-violation a11y spec, Phase 9 `plan_critic` injection, fleet-grid CSS parity); a Sesjon 18 re-review found 3 new defense-in-depth/conformance findings in the remediation code itself — deferred to v4.3.1 (a ready, plan-critic-reviewed Wave-4 plan exists). +v5.0.0 (breaking) **removes the bespoke playground.** v4.2/v4.3 shipped a ~388 KB bespoke playground SPA + `/trekrevise` + Handover 8 (annotation → revision); a browser walkthrough found it borderline unusable and it duplicated the official `/playground` plugin's `document-critique` / `diff-review` templates. The SPA, the `/trekrevise` command, Handover 8, the supporting `lib/` modules (`anchor-parser`, `annotation-digest`, `markdown-write`, `revision-guard`), the Playwright e2e suite, and the `@playwright/test` / `@axe-core/playwright` devDeps are all deleted. In their place: a small, zero-dependency `scripts/render-artifact.mjs` that renders any brief/plan/review `.md` to a self-contained, design-system-styled, zero-network `.html` (frontmatter folded into a `
` block). The producing commands call it on their last step and print the link; to annotate, run `/playground` (`document-critique`) on the `.md` and paste the generated prompt back — Claude revises the artifact freehand. Forks depending on the removed surfaces migrate to the `/playground` plugin. See `plugins/voyage/CHANGELOG.md` § v5.0.0. -v4.2.0 (non-breaking) adds the `/trekrevise` command and the **Handover 8 (annotation → revision)** annotation pipeline. The original `playground/voyage-playground.html` (single self-contained HTML with vendored `markdown-it` + `highlight.js`) lets operators paste a brief/plan/review, drag-select or hover-anchor comments, and export a `/trekrevise --apply` batch. Round-trippable in-place revision: byte-identical body outside anchor blocks (SC2), idempotent `annotation_digest` (SC3), additive frontmatter — no `*_version` bump, every brief / plan / review written before v4.2 validates as `revision: 0` without migration. Single-iteration MVP per research-05; multi-iteration loops deferred. Includes `lib/parsers/anchor-parser.mjs` + `lib/parsers/annotation-digest.mjs`, `lib/util/markdown-write.mjs` + `lib/util/revision-guard.mjs`, `scripts/render-artifact.mjs` server-side render CLI, and `docs/annotation-quickstart.md` ≤7-step operator walkthrough. +v4.0.0 (breaking) renamed the plugin from `ultraplan-local` to **Voyage** and all commands from `/ultra*-local` to `/trek*` to remove name collision with Anthropic's `/ultraplan` and `/ultrareview` features. See `plugins/voyage/TRADEMARKS.md` and `plugins/voyage/CHANGELOG.md`. -v4.0.0 (breaking) renames the plugin from `ultraplan-local` to **Voyage** and all seven commands from `/ultra*-local` to `/trek*` to remove name collision with Anthropic's `/ultraplan` and `/ultrareview` features. No migration path — fork-and-own users re-fork from main. See `plugins/voyage/TRADEMARKS.md` and `plugins/voyage/CHANGELOG.md`. - -Seven commands, one pipeline with clear division of labor: +Six commands, one pipeline with clear division of labor: - **`/trekbrief`** — Capture intent. Dynamic, quality-gated interview: a section-driven completeness loop (Phase 3) followed by a `brief-reviewer` stop-gate (Phase 4, max 3 review iterations). Required sections must reach an initial-signal gate AND pass review across completeness, consistency, testability, scope clarity, and research-plan validity before `brief.md` is written. Identifies research topics with copy-paste-ready `/trekresearch` commands. Optional auto-orchestration runs research + planning in foreground. Always interactive. - **`/trekresearch`** — Gather context. Deep multi-source research with triangulation: 5 local agents + 4 external agents + Gemini bridge, producing structured briefs with confidence ratings. Makes no build decisions. - **`/trekplan`** — Transform intent into an executable contract. Per-step YAML manifests (`expected_paths`, `commit_message_pattern`, `bash_syntax_check`). Plan-critic is a hard gate on manifest quality. Requires a task brief as input (`--brief` or `--project`). Auto-discovers `architecture/overview.md` when produced upstream and cross-references its `cc_features_proposed` against exploration findings. - **`/trekexecute`** — Execute the contract disciplined. Manifest-based verification, independent Phase 7.5 audit from git log + filesystem (ignores agent bookkeeping), Phase 7.6 bounded recovery dispatch for missing steps. Step 0 pre-flight catches sandbox push-denial before any work. `--validate` mode offers a fast schema-only sanity-check between planning and execution. - **`/trekreview`** — Close the iteration loop. Independent post-hoc reviewer reads `brief.md` from scratch and evaluates the diff produced by execute. Two parallel reviewers (brief-conformance + code-correctness) plus a Judge Agent (review-coordinator) for dedup and reasonableness filtering. Severity-tagged findings (Critical/High/Medium/Low/Info) with stable 40-char hex IDs feed back into planning via Handover 6 (`/trekplan --brief review.md` → remediation plan with `source_findings:` audit trail). -- **`/trekrevise`** — (v4.2) Apply operator annotations from the playground back into the source artifact. Operator opens `playground/voyage-playground.html`, anchors comments on `brief.md` / `plan.md` / `review.md`, exports a batch, pastes back as `/trekrevise --apply`. Round-trippable in-place revision via Handover 8: `revision:` counter, `source_annotations:` audit trail, deterministic `annotation_digest` (SHA-256 prefix). Idempotence — replaying the same batch yields identical digest with no body diff outside anchor refresh. Single-iteration MVP. - **`/trekcontinue`** — Zero-friction multi-session resumption. In a fresh chat, type `/trekcontinue` — reads `.session-state.local.json` (Handover 7), prints a 3-line summary, and immediately begins executing the next session. Any session-end mechanism may write the state file (`/trekexecute` Phase 8/2.55/4 do so automatically; `/trekendsession` helper writes it for informal flows). Forward-compat schema (unknown top-level keys ignored) so future producers can extend additively. -All artifacts land in one project directory: `.claude/projects/{YYYY-MM-DD}-{slug}/` contains `brief.md`, `research/NN-*.md`, `plan.md`, `sessions/`, `progress.json`, `review.md`, and `.session-state.local.json` (gitignored). `--project ` works across `/trekresearch`, `/trekplan`, `/trekexecute`, `/trekreview`, and (optionally) `/trekcontinue`. +`/trekbrief`, `/trekplan`, and `/trekreview` each finish by rendering their `.md` artifact to a self-contained `.html` next to it (`scripts/render-artifact.mjs` — zero deps, zero network) and printing the `file://` link. To annotate, run the official `/playground` plugin (`document-critique`) on the `.md` and paste its generated prompt back into the conversation. + +All artifacts land in one project directory: `.claude/projects/{YYYY-MM-DD}-{slug}/` contains `brief.md` (+ `brief.html`), `research/NN-*.md`, `plan.md` (+ `plan.html`), `sessions/`, `progress.json`, `review.md` (+ `review.html`), and `.session-state.local.json` (gitignored). `--project ` works across `/trekresearch`, `/trekplan`, `/trekexecute`, `/trekreview`, and (optionally) `/trekcontinue`. v3.4.0 (non-breaking) adds the **autonomy chain from brief approval to main-merge** plus parallel-wave hardenings. New `lib/util/autonomy-gate.mjs` state machine (`idle → approved → executing → merge-pending → main-merged`), `lib/review/plan-review-dedup.mjs` for Phase 9 inline dedup, `lib/stats/event-emit.mjs` for autonomy-gate transitions and main-merge gate, and `--gates {open|closed|adaptive}` flag on all four pipeline commands. `commands/trekplan.md` Phase 8 seals Opus-4.7 plan/list-emission schema-drift via `plan-validator --strict`. `commands/trekexecute.md` Phase 2.6 wave-executor adds 11 hardenings for plugin-in-monorepo + gitignored-state topology (GIT_OPTIONAL_LOCKS, --max-turns, --max-budget-usd, scoped --allowedTools, push-before-cleanup ordering). New `hooks/scripts/post-compact-flush.mjs` PostCompact hook re-injects session-state after compaction. SC7 synthetic determinism floor (Jaccard ≥ 0.833) for plan + review fixtures. Hook baseline regression pins. Architecture decision: Path B (sequential `--no-ff` parallel waves with manifest-driven failure recovery) ships; Path C (cache-first hybrid) deferred to v3.5.0 contingent on cache-telemetry harvest. @@ -119,9 +118,9 @@ v3.1.0 also adds: `docs/HANDOVER-CONTRACTS.md` as the single source of truth for Defense-in-depth security: plugin hooks block destructive commands and sensitive path writes, prompt-level denylist works in headless sessions, pre-execution plan scan catches dangerous commands before they run, scoped `--allowedTools` replaces `--dangerously-skip-permissions` in parallel sessions. Recommended hardening: `disableSkillShellExecution: true` for fork-ers handling untrusted plans (CC v2.1.91+). -Modes: default, brief-driven, project-scoped, research-enriched, foreground, quick, decompose, export, resume, revise +Modes: default, brief-driven, project-scoped, research-enriched, foreground, quick, decompose, export, resume -23 specialized agents · 7 commands (+ 1 helper) · 5 plugin hooks · 600+ tests · First marketplace playground (v4.2) · No cloud dependency +23 specialized agents · 6 commands (+ 1 helper) · 5 plugin hooks · 500+ tests · Self-contained HTML artifact rendering · No cloud dependency → [Full documentation](plugins/voyage/README.md) · [Migration guide](plugins/voyage/MIGRATION.md) diff --git a/plugins/voyage/.claude-plugin/plugin.json b/plugins/voyage/.claude-plugin/plugin.json index be88636..c9bfcf3 100644 --- a/plugins/voyage/.claude-plugin/plugin.json +++ b/plugins/voyage/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "voyage", - "description": "Voyage — brief, research, plan, execute, review, revise, continue. Contract-driven Claude Code pipeline + first marketplace playground.", - "version": "4.3.0", + "description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline; renders produced artifacts to HTML + link, annotate via the /playground plugin.", + "version": "5.0.0", "author": { "name": "Kjell Tore Guttormsen" }, diff --git a/plugins/voyage/.gitignore b/plugins/voyage/.gitignore index bc8c391..82487ef 100644 --- a/plugins/voyage/.gitignore +++ b/plugins/voyage/.gitignore @@ -3,6 +3,12 @@ Thumbs.db Desktop.ini +# Node / test artifacts +node_modules/ +test-results/ +playwright-report/ +blob-report/ + # Editor files *.swp *.swo diff --git a/plugins/voyage/CHANGELOG.md b/plugins/voyage/CHANGELOG.md index e5cb3bb..275a531 100644 --- a/plugins/voyage/CHANGELOG.md +++ b/plugins/voyage/CHANGELOG.md @@ -4,7 +4,58 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). -## 4.3.0 — 2026-05-10 — Playground rebuild: dashboard-centric + visual parity + anchor-rendering matures +## v5.0.0 — 2026-05-12 — Remove the bespoke playground; render artifacts to HTML + link + +**Breaking.** `/trekrevise` is removed. The `playground/` directory, Handover 8 +(annotation → revision), and all of its supporting `lib/` modules and tests are +gone. Forks that depended on `/trekrevise`, the playground HTML, `lib/parsers/anchor-parser.mjs`, +`lib/parsers/annotation-digest.mjs`, `lib/util/markdown-write.mjs`, or +`lib/util/revision-guard.mjs` must migrate to the official `/playground` plugin +(`document-critique` / `diff-review` templates). + +### Why + +v4.2/v4.3 built a ~388 KB bespoke playground SPA — vendored markdown-it + +highlight.js + DOMPurify, a design-system copy, a dashboard, drill-down, custom +annotation gestures, an anchor parser, and a Playwright e2e suite — to let +operators annotate brief/plan/review artifacts in a browser and fold those +annotations back via `/trekrevise` (Handover 8). A 2026-05-12 browser +walkthrough found it borderline unusable (annotation broken in the drill-down, +dashboard didn't take over, no in-context anchor markers, wrong anchor +derivation, broken `guide-panel` chrome). And it duplicated work the official +`/playground` plugin already does well: `document-critique` and `diff-review` +templates produce clean, self-contained single-file HTML for exactly this. The +NIH cost was real; the lasting value of v4.2/v4.3 is this cautionary record. +Lesson: reach for existing capabilities before building bespoke ones, and +walk the UI in a browser before shipping it. + +### Removed + +- **`plugins/voyage/playground/`** — the whole directory: `voyage-playground.html`, its `README.md`, vendored `lib/` (markdown-it / highlight.js / DOMPurify / manifests), and the vendored `playground-design-system/` copy (the canonical `shared/playground-design-system/` is untouched — other plugins still use it). +- **`/trekrevise`** — `commands/trekrevise.md` and the `trekrevise` block in `settings.json`. +- **Handover 8 (annotation → revision)** — deleted from `docs/HANDOVER-CONTRACTS.md`; back to seven handovers. +- **`lib/parsers/anchor-parser.mjs`**, **`lib/parsers/annotation-digest.mjs`**, **`lib/util/markdown-write.mjs`** (`readAndUpdate`), **`lib/util/revision-guard.mjs`**. +- **`scripts/vendor-playground-libs.mjs`**, **`playwright.config.mjs`**, **`tests/e2e/`** (a11y + network specs + snapshots), **`tests/playground/`**, **`tests/fixtures/playground/`**, **`tests/fixtures/screenshot-project/`**, **`tests/fixtures/annotation/`**. +- **Tests for the removed modules** — `tests/parsers/anchor-parser.test.mjs`, `tests/parsers/annotation-digest.test.mjs`, `tests/integration/annotation-roundtrip.test.mjs`, `tests/integration/annotation-block-boundary.test.mjs`, `tests/integration/annotation-export-schema.test.mjs`, `tests/integration/schema-rollback.test.mjs`, `tests/lib/revision-guard.test.mjs`, `tests/lib/markdown-write.test.mjs`, `tests/lib/source-annotations.test.mjs`, `tests/validators/{brief,plan,review}-validator-annotation-fields.test.mjs`, and the old `tests/scripts/render-artifact.test.mjs` (a fresh one ships in this release). +- **`docs/annotation-quickstart.md`**, **`docs/sc1-checklist-verification.md`**, **`docs/screenshots/`**. +- **`commands/trekplan.md` Phase 9 `plan_critic`-injection block** (and its `agents/planning-orchestrator.md` mirror) — it `import`ed the now-deleted `lib/util/markdown-write.mjs`. The `plan_critic` frontmatter field is no longer written. +- **`devDependencies` (`@axe-core/playwright`, `@playwright/test`)** and the `test:e2e` script in `package.json`; `package-lock.json` synced (no runtime deps — it's near-empty). +- The annotation-frontmatter HTML-comment preambles in `templates/{trekbrief,plan,trekreview}-template.md` (the validators still tolerate unknown frontmatter keys; nothing emits `revision:` / `source_annotations:` / `annotation_digest:` anymore). + +### Added + +- **`scripts/render-artifact.mjs`** — a small (~280 lines), zero-dependency Node renderer. Reads a `.md` artifact, folds frontmatter into a `
` block, renders a hand-rolled markdown subset (ATX headings, ordered/unordered/nested lists, fenced code blocks, inline code, bold/italic, links, blockquotes, GitHub-style tables, horizontal rules), and emits a self-contained HTML file with an inlined, design-system-aligned stylesheet (light + dark + print). **Zero external network, zero build step, deterministic** (byte-identical on re-run). CLI: `node scripts/render-artifact.mjs [--out ]`; also `npm run render`. +- **Render-and-link step** at the end of `/trekbrief`, `/trekplan`, and `/trekreview` — each renders its just-written `.md` to `{project_dir}/{artifact}.html` and prints the `file://` link plus a one-liner: to annotate, run the `/playground` plugin (`document-critique`) on the `.md` and paste the generated prompt back; Claude revises the artifact freehand. +- **`tests/scripts/render-artifact.test.mjs`** (fresh, 8 tests) — self-contained-document shape, frontmatter `
` folding, title derivation, headings/code-fences/lists/tables/blockquotes rendering, HTML escaping, determinism, default output path, arg parsing. +- **`tests/lib/doc-consistency.test.mjs` v5.0.0 pins** — `playground/` gone, `commands/trekrevise.md` gone, Handover 8 gone, `render-artifact.mjs` exists, producing commands reference `render-artifact.mjs` and `/playground`, CHANGELOG has a v5.0.0 entry and retains the v4.2.0 entry, and no source file (outside CHANGELOG) references `trekrevise`. + +### Notes + +- Resolves the three v4.3.1-deferred findings as moot: `87069b35` and `c6c64a58` targeted `playground/voyage-playground.html` (deleted); `4cc3bfc9` targeted the Phase 9 `readAndUpdate` block in `commands/trekplan.md` (deleted). +- Command count: seven → six (`/trekbrief`, `/trekresearch`, `/trekplan`, `/trekexecute`, `/trekreview`, `/trekcontinue`; plus the `/trekendsession` helper). +- `npm test` is the fork-readiness gate; `npm run test:e2e` is gone (no Playwright). + +## v4.3.0 — 2026-05-10 — Playground rebuild: dashboard-centric + visual parity + anchor-rendering matures **Additive. No breaking changes. Forward-compat with every brief / plan / review / playground export written before v4.3.** @@ -51,13 +102,13 @@ After the rebuild, an independent `/trekreview` (Sesjon 13) flagged 11 findings - `IndexedDB` primary persistence (localStorage stays primary for v4.3). - Hybrid claude-design-skill → canvas → frontend-design workflow (research/02 deferred to v4.4+). -### Known issues — deferred to v4.3.1 +### Known issues — deferred to v4.3.1 → all resolved in v5.0.0 (moot) -The Sesjon 18 re-review surfaced 3 findings in code the Sesjon 13–18 remediation introduced. None is a live operator-facing exploit; they are deferred to a v4.3.1 patch (a ready Wave-4 remediation plan exists, plan-critic-reviewed, ALIGNED): +The Sesjon 18 re-review surfaced 3 findings in code the Sesjon 13–18 remediation introduced. They were deferred to a planned v4.3.1 patch; **v5.0.0 makes all three moot** by removing the playground entirely: -- **`87069b35` (SECURITY_INJECTION, defense-in-depth)** — `renderScreenshotGallery()` interpolates `screenshots[].dataUrl` raw into an `` attribute, and `renderDashboard`'s `innerHTML` (unlike `renderArtifact`'s) is not DOMPurify-wrapped. **Not exploitable from the operator file-load path** — `FileReader.readAsDataURL` of a `.png` File always yields a safe base64 `data:` URL; the only injection path is `window.__voyage.scheduleRender({ artifacts: { screenshots: [{ dataUrl: '" onerror="…' }] } })`, which requires JS already executing in the page. Fix: a `data:image/…;base64,…` allowlist in `renderScreenshotGallery`. -- **`4cc3bfc9` (PLAN_EXECUTE_DRIFT)** — `commands/trekplan.md:745` uses a backtick template literal as an ES `import` specifier (a `SyntaxError`). An LLM running the Phase 9 snippet verbatim fails before `plan_critic` is written; the documented fallback is non-fatal, so `plan.md` is still produced — without the field. Fix: backtick → single quote. -- **`c6c64a58` (MISSING_TEST)** — no test asserts that a non-`data:` `dataUrl` is neutralised before DOM injection, so `87069b35` is invisible to `npm test`. Fix: Group D Playwright runtime guard + Group A static-grep guard. +- **`87069b35` (SECURITY_INJECTION, defense-in-depth)** — `renderScreenshotGallery()` interpolated `screenshots[].dataUrl` raw into an ``. **Moot in v5.0.0** — `playground/voyage-playground.html` is deleted. +- **`4cc3bfc9` (PLAN_EXECUTE_DRIFT)** — `commands/trekplan.md` Phase 9 used a backtick template literal as an ES `import` specifier (`SyntaxError`). **Moot in v5.0.0** — the Phase 9 `plan_critic`-injection-via-`readAndUpdate` block is deleted. +- **`c6c64a58` (MISSING_TEST)** — no test covered the gallery `dataUrl` injection path. **Moot in v5.0.0** — the gallery and its host file are deleted. ### Notes diff --git a/plugins/voyage/CLAUDE.md b/plugins/voyage/CLAUDE.md index 7bd5d4e..8f9c535 100644 --- a/plugins/voyage/CLAUDE.md +++ b/plugins/voyage/CLAUDE.md @@ -15,7 +15,6 @@ Voyage — a contract-driven Claude Code pipeline: brief, research, plan, execut | `/trekplan` | Plan — brief-reviewer, explore, plan, review. Requires `--brief` or `--project`. Auto-discovers `architecture/overview.md` if present | opus | | `/trekexecute` | Execute — disciplined plan/session-spec executor with failure recovery | opus | | `/trekreview` | Review — independent post-hoc review of delivered code against the brief. Produces `review.md` with severity-tagged findings (Handover 6) | opus | -| `/trekrevise` | Revise — apply operator annotations from the playground back into brief/plan/review with audit trail (Handover 8). Requires `--project` | opus | | `/trekcontinue` | Continue — resumes the next session of a multi-session voyage project. Reads `.session-state.local.json` (Handover 7) and immediately begins executing | opus | | `/trekendsession` | End-session — mark the current session complete and write session-state pointing at the next session. Helper for informal multi-session flows | sonnet | @@ -233,26 +232,27 @@ Local Docker Compose stack: `examples/observability/`. Operator docs: `docs/obse **Continue:** `/trekcontinue` reads `{dir}/.session-state.local.json` (Handover 7), validates schema-v1 via `session-state-validator`, narrates a 3-line summary (project / next-session-label / brief-path), and immediately begins executing the next session. Auto-discovers active project state files under `.claude/projects/*/.session-state.local.json` if no explicit `` argument. Operator-invoked only — never auto-loaded via SessionStart. The `/trekendsession` helper is the informal-flow producer: writes the same state file for ad-hoc multi-session handovers that don't run through `/trekexecute`. -**Revise (v4.2):** `/trekrevise --project ` consumes a batch exported from `playground/voyage-playground.html` and folds operator annotations back into the source artifact (`brief.md` / `plan.md` / `review.md`). Phase 1 parse + validate, Phase 2 read source + rollback hygiene (`*.local.bak` via `lib/util/revision-guard.mjs`), Phase 3 parse anchors + validate placement (`lib/parsers/anchor-parser.mjs` — block-boundary discipline), Phase 4 compute revision diff + deterministic SHA-256 digest (`lib/parsers/annotation-digest.mjs`), Phase 5 atomic apply via `lib/util/markdown-write.mjs`, Phase 6 round-trip integrity (`stripAnchors`-of-written equals pre-write body), Phase 7 optional review-gate when target is plan and review.md exists, Phase 8 stats + report. Single-iteration MVP — each batch produces one `revision:` increment with `source_annotations:` audit trail. **Handover 8** (`docs/HANDOVER-CONTRACTS.md`) — additive frontmatter; no `*_version` bump; artifacts written before v4.2 validate as `revision: 0`. - -**Playground (v4.3):** `playground/voyage-playground.html` is the operator-facing surface for browsing voyage projects and editing annotations. v4.3 rebuilds the v4.2 playground from the ground up — dashboard-centric layout (`fleet-grid` of `fleet-tile` per artifact), file://-loader with three entry points (`webkitdirectory` directory-picker / drag-drop with `webkitGetAsEntry` recursive walk / URL-parameter `?project=/abs/path`), block-boundary anchor placement matching browser-side `parseAnchor` to Node-side `lib/parsers/anchor-parser.mjs` regex, hidden-by-default sidebar-rail with J/K keyboard navigation, two-opacity pattern (active 100% / inactive 40% / resolved 30% strikethrough), A11Y panel built from DS-primitives, screenshots-spor convention via `window.__voyage` hooks (`navigate` / `scheduleRender` / `getProjectArtifacts`) + `docs/screenshots/`. Path-traversal + symlink/dotfile filter (`isProjectPathSafe`) blocks `..` / `node_modules/` / `dist/` / `build/` / hidden-paths with `aria-live` announces. DOMPurify ≥ 3.1.1 vendored for `sanitizeAnnotation`; HTML-comment indirect prompt-injection mitigation via `parseAnchor`-allowlist gate before `md.render`. Total bundle 388 KB / 460 KB HALT-gate. Test pyramid Groups A-D — Group A 17 static-HTML tests (SC1 10-element checklist + SC3 + SC6 + SC7 tag-level no-CDN), Group B 9 DS-token + theme-toggle + sidebar-tab + keyboard-pattern tests, Group C 7 export-bundle schema + `annotation_digest` SHA-256 validity tests, Group D 5 Playwright e2e specs (light/dark axe-core absolute zero-violation + pixel-diff smoke + inline-gallery + zero-external-network authoritative gate). See `playground/README.md` + `docs/sc1-checklist-verification.md`. A Sesjon 13–18 independent-review remediation cycle closed all 11 findings from the first `/trekreview` (DOMPurify on the artifact body, inline `renderScreenshotGallery`, absolute-zero-violation a11y spec, Phase 9 `plan_critic` injection, fleet-grid CSS parity). **v4.3.0 ships with 3 new re-review findings deferred to v4.3.1:** `87069b35` (`renderScreenshotGallery` interpolates `screenshots[].dataUrl` raw into an `` — defense-in-depth only, *not* exploitable from the operator file-load path, only via the `window.__voyage.scheduleRender` hook), `4cc3bfc9` (backtick template-literal ES-import specifier at `commands/trekplan.md:745` Phase 9 — SyntaxError; documented fallback keeps `plan.md` valid, just without `plan_critic`), `c6c64a58` (no test for the gallery `dataUrl` injection path). A plan-critic-reviewed Wave-4 remediation plan is ready; see [CHANGELOG.md](CHANGELOG.md) § "Known issues — deferred to v4.3.1". +**Render-and-link (v5.0.0):** the last step of `/trekbrief`, `/trekplan`, and `/trekreview` renders the just-written `.md` artifact to a self-contained `.html` in the same project directory (`scripts/render-artifact.mjs` — zero npm deps, zero external network, design-system-styled, frontmatter folded into a `
` block) and prints the `file://` link. To annotate, the operator runs the official `/playground` plugin (`document-critique` template) on the `.md` and pastes the generated prompt back into the conversation; Claude revises the artifact freehand. This replaces the v4.2/v4.3 bespoke playground SPA + `/trekrevise` + Handover 8 (annotation → revision), all removed in v5.0.0 — see [CHANGELOG.md](CHANGELOG.md) § v5.0.0 for why (the bespoke playground duplicated capabilities the official `/playground` plugin already provides). **Security:** 4-layer defense-in-depth: plugin hooks (pre-bash-executor, pre-write-executor), prompt-level denylist (works in headless sessions), pre-execution plan scan (Phase 2.4), scoped `--allowedTools` replacing `--dangerously-skip-permissions`. Hard Rules 14-16 enforce verify command security, repo-boundary writes, and sensitive path protection. -**Pipeline:** `/trekbrief` produces the task brief. `/trekresearch --project ` fills in `{dir}/research/`. `/trekplan --project ` reads brief + research to produce `{dir}/plan.md` (and auto-discovers `{dir}/architecture/overview.md` if an opt-in upstream architect plugin produced one). `/trekexecute --project ` executes and writes `{dir}/progress.json`. `/trekreview --project ` produces `{dir}/review.md`. `/trekrevise --project ` (v4.2) folds operator annotations back into any of the three artifacts in-place, with audit trail in frontmatter. All artifacts live in one project directory. +**Pipeline:** `/trekbrief` produces the task brief. `/trekresearch --project ` fills in `{dir}/research/`. `/trekplan --project ` reads brief + research to produce `{dir}/plan.md` (and auto-discovers `{dir}/architecture/overview.md` if an opt-in upstream architect plugin produced one). `/trekexecute --project ` executes and writes `{dir}/progress.json`. `/trekreview --project ` produces `{dir}/review.md`. `/trekbrief`, `/trekplan`, and `/trekreview` each render their artifact to `{dir}/{artifact}.html` and print the link (annotate via the `/playground` plugin). All artifacts live in one project directory. **Project-directory contract (v3.0.0):** trekplan owns the directory layout below. The `architecture/` subdirectory is opt-in and produced by an opt-in upstream architect plugin (not bundled) — the architect plugin is no longer publicly distributed, but the `architecture/overview.md` slot remains available for any compatible producer. ``` .claude/projects/{YYYY-MM-DD}-{slug}/ - brief.md ← trekbrief writes; everyone reads; trekrevise mutates in-place + brief.md ← trekbrief writes; everyone reads + brief.html ← trekbrief renders (self-contained; for browser viewing / /playground) research/*.md ← trekresearch writes; plan + architect read architecture/ ← OPT-IN, owned by an opt-in upstream architect plugin (not bundled) overview.md gaps.md - plan.md ← trekplan writes; trekexecute reads; trekrevise mutates in-place + plan.md ← trekplan writes; trekexecute reads + plan.html ← trekplan renders progress.json ← trekexecute writes - review.md ← trekreview writes; trekplan reads (Handover 6); trekrevise mutates in-place + review.md ← trekreview writes; trekplan reads (Handover 6) + review.html ← trekreview renders ``` No code-level dependency between plugins — the contract is filesystem-level only. @@ -261,13 +261,13 @@ No code-level dependency between plugins — the contract is filesystem-level on All artifacts in one project directory (default): - Project root: `.claude/projects/{YYYY-MM-DD}-{slug}/` - - `brief.md` (task brief from `/trekbrief`) + - `brief.md` + `brief.html` (task brief from `/trekbrief`; `.html` is the self-contained rendered view) - `research/{NN}-{slug}.md` (research briefs from `/trekresearch --project`) - `architecture/overview.md` + `architecture/gaps.md` (opt-in, produced by an opt-in upstream architect plugin, not bundled) - - `plan.md` (from `/trekplan --project`) + - `plan.md` + `plan.html` (from `/trekplan --project`) - `sessions/session-*.md` (from `--decompose`) - `progress.json` (from `/trekexecute --project`) - - `review.md` (from `/trekreview --project`) + - `review.md` + `review.html` (from `/trekreview --project`) - `.session-state.local.json` (Handover 7 — gitignored via `*.local.json`; written by `/trekexecute` Phase 8/2.55/4 or `/trekendsession`; read by `/trekcontinue`) Legacy paths (still work without `--project`): diff --git a/plugins/voyage/README.md b/plugins/voyage/README.md index 2be2c3b..d7fe3f9 100644 --- a/plugins/voyage/README.md +++ b/plugins/voyage/README.md @@ -1,6 +1,6 @@ # trekplan — Brief, Research, Plan, Execute, Review, Continue -![Version](https://img.shields.io/badge/version-4.3.0-blue) +![Version](https://img.shields.io/badge/version-5.0.0-blue) ![License](https://img.shields.io/badge/license-MIT-green) ![Platform](https://img.shields.io/badge/platform-Claude%20Code-purple) @@ -8,7 +8,7 @@ *AI-generated: all code produced by Claude Code through dialog-driven development. [Full disclosure →](../../README.md#ai-generated-code-disclosure)* -A [Claude Code](https://docs.anthropic.com/en/docs/claude-code) plugin for deep implementation planning, multi-source research, autonomous execution, independent post-hoc review, operator-driven artifact annotation, and zero-friction multi-session resumption. Seven commands, one pipeline: +A [Claude Code](https://docs.anthropic.com/en/docs/claude-code) plugin for deep implementation planning, multi-source research, autonomous execution, independent post-hoc review, and zero-friction multi-session resumption. Six commands, one pipeline: | Command | What it does | |---------|-------------| @@ -17,10 +17,11 @@ A [Claude Code](https://docs.anthropic.com/en/docs/claude-code) plugin for deep | **`/trekplan`** | Plan — agent swarm exploration, Opus planning, adversarial review | | **`/trekexecute`** | Execute — disciplined step-by-step implementation with failure recovery | | **`/trekreview`** | Review — independent post-hoc review of delivered code against the brief, severity-tagged findings | -| **`/trekrevise`** | Revise — apply operator annotations from the playground back into brief/plan/review with audit trail (Handover 8, v4.2) | | **`/trekcontinue`** | Continue — read `.session-state.local.json` and resume the next session in a multi-session project | -Every artifact lives in one project directory: `.claude/projects/{YYYY-MM-DD}-{slug}/` contains `brief.md`, `research/NN-*.md`, `plan.md`, `sessions/`, `progress.json`, and `review.md`. +`/trekbrief`, `/trekplan`, and `/trekreview` also render their artifact to a self-contained `.html` next to it and print the `file://` link — annotate via the official `/playground` plugin (`document-critique`) and paste its prompt back. + +Every artifact lives in one project directory: `.claude/projects/{YYYY-MM-DD}-{slug}/` contains `brief.md` (+ `brief.html`), `research/NN-*.md`, `plan.md` (+ `plan.html`), `sessions/`, `progress.json`, and `review.md` (+ `review.html`). ### Division of labor @@ -31,7 +32,6 @@ Every artifact lives in one project directory: `.claude/projects/{YYYY-MM-DD}-{s | `/trekplan` | **Transform intent into an executable contract** — per-step YAML manifest, regex-validated checkpoints, verifiable paths. Plan-critic is a hard gate. Auto-discovers `architecture/overview.md` as priors when an opt-in upstream architect plugin (not bundled) is installed. | `plan.md` with Manifest blocks + `plan_version: 1.7` | | `/trekexecute` | **Execute the contract disciplined** — fresh verification, independent manifest audit, honest reporting. Does NOT compensate for weak plans — escalates. | `progress.json` + structured report + manifest-audit status | | `/trekreview` | **Close the loop** — independent post-hoc reviewer reads `brief.md` and the diff produced by execute, runs brief-conformance + code-correctness reviewers in parallel, dedups via Judge Agent. Severity-tagged findings (Critical/High/Medium/Low/Info) feed back into planning via Handover 6. | `review.md` (`type: trekreview`) with stable 40-char hex finding-IDs | -| `/trekrevise` | **Re-fold operator annotations** — operator opens the playground, anchors comments on a brief/plan/review, exports the batch, pastes back as `/trekrevise --apply`. Round-trippable in-place revision: `revision:` counter, `source_annotations:` audit trail, deterministic `annotation_digest`. Single-iteration MVP per Handover 8. | revised `brief.md` / `plan.md` / `review.md` with frontmatter audit trail | **Principle:** Each step consumes the previous step's structured artifact. If execute has to guess, the plan is weak and must be revised upstream — not patched downstream. @@ -503,65 +503,37 @@ Both arguments are required. No interactive prompt — headless-safe. --- -## `/trekrevise` — Annotation playground (v4.2 + v4.3 rebuild) +## Rendered artifacts & annotation (v5.0.0) -Voyage's interactive playground. Open `playground/voyage-playground.html` in -any modern browser, point it at a `.claude/projects//`-mappe, and -browse + annotate every artifact (brief / plan / research / review) directly. -The playground is a single self-contained HTML file with vendored -`markdown-it` + `highlight.js` + `DOMPurify` ≥ 3.1.1 — no build step, no -network calls, no telemetry. +`/trekbrief`, `/trekplan`, and `/trekreview` each finish by rendering their +just-written `.md` to a self-contained `.html` next to it +(`{project_dir}/brief.html`, `plan.html`, `review.html`) and printing the +`file://` link. The renderer (`scripts/render-artifact.mjs`) is a small, +zero-dependency Node script: it folds frontmatter into a `
` block, +puts code fences in styled `
`, renders tables/lists/links, and inlines a
+compact design-system-aligned stylesheet. **No external network, no build
+step, no telemetry.** Two runs on the same input produce byte-identical HTML.
 
-**v4.3 rebuild** (2026-05-10) — dashboard-centric layout with `fleet-grid` of
-`fleet-tile` per artifact and drill-down detail surface. Three file://-loader
-entry points: `webkitdirectory` directory-picker (Chromium primary, FF150+
-secondary), drag-drop with `webkitGetAsEntry` recursive walk, and URL-parameter
-`?project=/abs/path` ergonomic shortcut. Hidden-by-default sidebar-rail with
-J/K keyboard navigation + Esc dismiss; two-opacity pattern (active 100% /
-inactive 40% / resolved 30% strikethrough). A11Y panel built from DS-primitives.
-Path-traversal + symlink/dotfile filter (`isProjectPathSafe`) blocks
-`..` / `node_modules/` / `dist/` / `build/` / hidden-paths. Total bundle 388 KB
-under 460 KB HALT-gate. Test pyramid Groups A-D — 17 static-HTML + 9 structure +
-7 schema/digest tests (`npm test`) plus 4 Playwright e2e (light/dark axe-core
-delta-baseline + pixel-diff smoke + zero-external-network gate via
-`npm run test:e2e`). See [`playground/README.md`](playground/README.md) +
-[`docs/sc1-checklist-verification.md`](docs/sc1-checklist-verification.md).
-
-The annotation lifecycle is **Handover 8 — annotation → revision** (see
-`docs/HANDOVER-CONTRACTS.md`). Three stages:
-
-1. **Annotate** — drag-select text or hover-anchor a paragraph; fill the
-   modal with comment + intent (`change` / `add` / `remove` / `clarify` /
-   `risk`). Anchors are placed only at block boundaries; the playground
-   refuses placement inside list items, mid-paragraph, or at line-start
-   collision points.
-2. **Export** — click "Eksporter batch" to copy a complete
-   `/trekrevise --project  --apply '{...JSON...}'` invocation to your
-   clipboard.
-3. **Apply** — paste the command in Claude Code chat. `/trekrevise` parses
-   the batch, re-runs placement validation, atomically writes anchor comment
-   blocks back into the source artifact, increments `revision:`, appends to
-   `source_annotations:`, and recomputes the deterministic `annotation_digest`.
-
-The artifact's body remains byte-identical outside anchor blocks (SC2). Re-applying
-the same batch yields the same digest (idempotence — SC3). The frontmatter
-audit trail makes every revision traceable in git.
+To **annotate** an artifact, run the official `/playground` plugin
+(`document-critique` template) on the `.md` file and paste the prompt it
+generates back into the conversation — Claude then revises the artifact
+freehand from your notes. The `/playground` plugin already produces clean,
+self-contained single-file HTML for exactly this; voyage no longer ships its
+own annotation UI.
 
 ```bash
-# Open the playground (one-time)
-open plugins/voyage/playground/voyage-playground.html
-# Or render an artifact server-side via the CLI helper:
+# Render any artifact manually (the producing commands do this automatically):
 node plugins/voyage/scripts/render-artifact.mjs \
-  .claude/projects/2026-05-09-feature/plan.md \
-  --out /tmp/plan.html
+  .claude/projects/2026-05-09-feature/plan.md
+# → writes .claude/projects/2026-05-09-feature/plan.html, prints the path
 ```
 
-For a step-by-step walkthrough, see
-[`docs/annotation-quickstart.md`](docs/annotation-quickstart.md).
-
-**Single-iteration MVP:** Each operator batch produces one `revision:` bump.
-Multi-iteration loops (revise → re-review → revise again) are deferred
-indefinitely per research-05; the brief's SC4 wording is single-revision.
+> **Removed in v5.0.0.** v4.2/v4.3 shipped a ~388 KB bespoke playground SPA +
+> `/trekrevise` + Handover 8 (annotation → revision). A browser walkthrough
+> found it borderline unusable, and it duplicated the official `/playground`
+> plugin's `document-critique` / `diff-review` templates. All of it — the SPA,
+> the command, the supporting `lib/` modules, the anchor parser, the Playwright
+> e2e suite — was deleted. See [CHANGELOG.md](CHANGELOG.md) § v5.0.0.
 
 ---
 
@@ -685,7 +657,7 @@ Borrowed pattern from `llm-security` (commit `97c5c9d`); extending the plugin sh
 
 ### Handover contracts
 
-`docs/HANDOVER-CONTRACTS.md` is the single source of truth for the file formats that pass between the four pipeline commands (brief → research → plan → execute). When you fork the plugin or extend a stage, that document tells you what every producer must write and what every consumer is allowed to assume. It also documents the *external* contract for `architecture/overview.md` (owned by an opt-in upstream architect plugin, not bundled) — discovery only, drift-warn never drift-fail.
+`docs/HANDOVER-CONTRACTS.md` is the single source of truth for the file formats that pass between the pipeline commands (seven handovers: brief → research, research → plan, architecture → plan, plan → execute, progress.json resume, review → plan, `.session-state.local.json`). When you fork the plugin or extend a stage, that document tells you what every producer must write and what every consumer is allowed to assume. It also documents the *external* contract for `architecture/overview.md` (owned by an opt-in upstream architect plugin, not bundled) — discovery only, drift-warn never drift-fail.
 
 ### PreCompact resume integrity (CC v2.1.105+)
 
@@ -695,7 +667,7 @@ The `pre-compact-flush.mjs` hook directly fixes the documented P0 in `docs/treke
 
 **Infrastructure-as-code (IaC) gets reduced value.** The exploration agents are designed for application code. Terraform, Helm, Pulumi, CDK projects will get a plan, but agents like `architecture-mapper` and `test-strategist` produce less useful output for IaC. Use trekplan for the structural plan, then supplement IaC-specific steps manually.
 
-**v4.3.0 — 3 known re-review findings deferred to v4.3.1.** A Sesjon 13–18 independent-review remediation closed all 11 findings from the first `/trekreview`, but a re-review found 3 new ones in the remediation code: `87069b35` (`renderScreenshotGallery` interpolates `screenshots[].dataUrl` raw into an `` — defense-in-depth only; *not* exploitable from the operator file-load path, only via the `window.__voyage.scheduleRender` hook which needs JS already running in the page), `4cc3bfc9` (a backtick template literal as an ES `import` specifier in `commands/trekplan.md:745` Phase 9 — SyntaxError; the documented fallback keeps `plan.md` valid, just without `plan_critic`), and `c6c64a58` (no test covers the gallery `dataUrl` injection path). A plan-critic-reviewed Wave-4 remediation plan is ready; v4.3.1 ships the fixes. See [CHANGELOG.md](CHANGELOG.md) § "Known issues — deferred to v4.3.1".
+**Rendered HTML is read-only.** `scripts/render-artifact.mjs` produces a static, self-contained view for browsing — it is not an editor. To revise an artifact from operator feedback, run the `/playground` plugin (`document-critique`) on the `.md` and paste its prompt back. The markdown subset the renderer supports covers what the artifact templates emit (headings, lists, code fences, tables, links, blockquotes, bold/italic, inline code); exotic markdown extensions are not rendered.
 
 ## Installation
 
diff --git a/plugins/voyage/agents/planning-orchestrator.md b/plugins/voyage/agents/planning-orchestrator.md
index a2bf7af..d7f6abb 100644
--- a/plugins/voyage/agents/planning-orchestrator.md
+++ b/plugins/voyage/agents/planning-orchestrator.md
@@ -431,17 +431,6 @@ After both complete:
   machine-checkable — a plan without verifiable manifests cannot drive
   deterministic execution.
 - Add a "Revisions" note at the bottom documenting changes
-- **Inject `plan_critic` verdict into plan frontmatter** (after dedup, after
-  any blocker/major revisions). Read the verdict from
-  `/tmp/plan-critic-out.json` and atomically update plan.md via
-  `readAndUpdate` from `lib/util/markdown-write.mjs`:
-  `frontmatter.plan_critic = criticVerdict`. Field semantics: one of
-  `APPROVE`, `APPROVE_WITH_NOTES`, `REVISE`, or `BLOCK`. The
-  `plan_critic` field is additive + optional — plans without it remain
-  valid. Surfaces (notably `playground/voyage-playground.html`
-  `buildArtifactKeyStat`) read this field directly without re-parsing
-  the review JSON. Detailed contract documented in
-  `commands/trekplan.md` Phase 9 (906f155d, bee33a69).
 
 ### Phase 7 — Completion
 
diff --git a/plugins/voyage/commands/trekbrief.md b/plugins/voyage/commands/trekbrief.md
index 8ee392c..288b148 100644
--- a/plugins/voyage/commands/trekbrief.md
+++ b/plugins/voyage/commands/trekbrief.md
@@ -483,13 +483,30 @@ If the validator returns errors, report them to the user and offer to
 re-enter Phase 4 with the validator's hints in scope. If only warnings,
 note them in the final report.
 
+**Render to HTML + link (annotation via /playground):** after `brief.md`
+is final, render it to a self-contained HTML view in the same directory:
+
+```bash
+node ${CLAUDE_PLUGIN_ROOT}/scripts/render-artifact.mjs "{PROJECT_DIR}/brief.md"
+```
+
+This writes `{PROJECT_DIR}/brief.html` — zero-network, design-system-styled
+(frontmatter folded into a `
` block). If it exits non-zero, surface +a one-line warning and continue — the rendered view is a convenience, not a +gate. + Report: ``` Brief written: {PROJECT_DIR}/brief.md +Brief rendered: file://{abs path to brief.html} Review iterations: {1..3} Final quality: {complete | partial} Validator: {PASS | warnings(N)} Research topics identified: {N} + +To annotate: open brief.html, then run the `/playground` plugin +(document-critique template) on brief.md and paste the generated +prompt back here. Claude revises brief.md freehand from your notes. ``` ## Phase 5 — Auto-orchestration opt-in (if research_topics > 0) diff --git a/plugins/voyage/commands/trekplan.md b/plugins/voyage/commands/trekplan.md index ed5d388..3f30d08 100644 --- a/plugins/voyage/commands/trekplan.md +++ b/plugins/voyage/commands/trekplan.md @@ -726,47 +726,6 @@ After both complete: - If only **minor** issues or clean: proceed without changes. Note the review result in the plan. -### Inject plan_critic verdict into plan frontmatter (post-dedup, post-revise) - -After the dedup pass completes and any blocker/major revisions are folded -in, atomically update the plan's frontmatter with the plan-critic verdict -so downstream surfaces (notably `playground/voyage-playground.html` -`buildArtifactKeyStat`) can read it without re-parsing the review JSON. - -Phase 8 (write plan) precedes Phase 9 (adversarial review), so the -verdict cannot be in Phase 8's frontmatter template — it must be -injected here, after the verdict exists. - -Read the verdict from `/tmp/plan-critic-out.json` and apply it via -`lib/util/markdown-write.mjs` `readAndUpdate`: - -```js -import { readFileSync } from 'node:fs'; -import { readAndUpdate } from `${CLAUDE_PLUGIN_ROOT}/lib/util/markdown-write.mjs`; - -const criticVerdict = JSON.parse(readFileSync('/tmp/plan-critic-out.json', 'utf-8')).verdict; -const result = readAndUpdate(planPath, ({ frontmatter, body }) => { - frontmatter.plan_critic = criticVerdict; - return { frontmatter, body }; -}); -if (!result.valid) { - // Non-fatal: log warning. plan.md already exists from Phase 8; - // missing plan_critic is degraded UX, not blocking. - console.warn('plan_critic injection failed:', result.errors); -} -``` - -Field semantics: -- `plan_critic` — string. One of `APPROVE`, `APPROVE_WITH_NOTES`, - `REVISE`, or `BLOCK`. Matches the rubric in `plan-critic` agent - output. Omitted when the inject failed (Phase 9 surfaces a warning - in that case but does not abort). - -Schema compatibility: `plan_critic` is **additive and optional**. The -plan validator (`lib/validators/plan-validator.mjs`) tolerates unknown -frontmatter keys, so this addition does NOT require a `plan_version` -bump. Plans written before this field was added validate identically. - ## Phase 10 — Present and refine Present a summary to the user: @@ -810,6 +769,29 @@ If the user asks questions or requests changes: - Show what changed - Re-present the summary +### Render to HTML + link (annotation via /playground) + +After `plan.md` is final, render it to a self-contained HTML view in the +same project directory and print the `file://` link: + +```bash +node ${CLAUDE_PLUGIN_ROOT}/scripts/render-artifact.mjs "{plan_path}" +``` + +This writes `{plan_dir}/plan.html` — a zero-network, design-system-styled +page (frontmatter folded into a `
` block, code fences in styled +`
`). Print:
+
+```
+Plan rendered: file://{abs path to plan.html}
+To annotate: open it, then run the `/playground` plugin
+(document-critique template) on plan.md and paste the generated
+prompt back here. Claude revises plan.md freehand from your notes.
+```
+
+If `render-artifact.mjs` exits non-zero, surface a one-line warning and
+continue — the rendered view is a convenience, not a gate.
+
 ## Phase 11 — Handoff
 
 ### "save" / "later" / "done"
diff --git a/plugins/voyage/commands/trekreview.md b/plugins/voyage/commands/trekreview.md
index 94da08c..a02c59a 100644
--- a/plugins/voyage/commands/trekreview.md
+++ b/plugins/voyage/commands/trekreview.md
@@ -262,6 +262,17 @@ Append a stats line to `${CLAUDE_PLUGIN_DATA}/trekreview-stats.jsonl`
 If `${CLAUDE_PLUGIN_DATA}` is unset or not writable, skip stats silently.
 Never let stats failures block the main workflow.
 
+**Render to HTML + link (annotation via /playground):** after `review.md`
+is final, render it to a self-contained HTML view in the same directory:
+
+```bash
+node ${CLAUDE_PLUGIN_ROOT}/scripts/render-artifact.mjs "{review_path}"
+```
+
+This writes `{project_dir}/review.html` — zero-network, design-system-styled.
+If it exits non-zero, surface a one-line warning and continue — the rendered
+view is a convenience, not a gate.
+
 ## Phase 8.5 — Validate-only mode (`--validate`)
 
 When `mode == validate`:
@@ -282,6 +293,7 @@ After the write succeeds, print:
 **Brief:** {brief_path}
 **Project:** {project_dir}
 **Review:** {review_path}
+**Rendered:** file://{abs path to review.html}
 **Scope:** {before_sha}..{after_sha} ({reviewed_files_count} files)
 **Verdict:** {BLOCK | WARN | ALLOW}
 
@@ -297,9 +309,11 @@ After the write succeeds, print:
 {up to 5 highest-severity findings}
 
 You can:
-- Read the full review at {review_path}
+- Read the full review at {review_path} (or open review.html in a browser)
 - Feed BLOCKER + MAJOR findings into a follow-up plan:
     /trekplan --brief {review_path}
+- Annotate: run the `/playground` plugin (document-critique template) on
+  review.md and paste the generated prompt back here
 - Re-run with `--quick` for a faster correctness-only pass
 - Re-run with `--since ` to narrow scope
 ```
diff --git a/plugins/voyage/commands/trekrevise.md b/plugins/voyage/commands/trekrevise.md
deleted file mode 100644
index 698d66c..0000000
--- a/plugins/voyage/commands/trekrevise.md
+++ /dev/null
@@ -1,508 +0,0 @@
----
-name: trekrevise
-description: |
-  Apply operator-annotated brief/plan/review back into the source artifact
-  with audit trail. Reads anchored comments from pasted (or `--from-file`)
-  content, validates anchor placement, computes a canonical
-  annotation_digest, and writes the revision in-place with
-  rollback-on-validator-fail. Implements Handover 8 (annotation → revision).
-argument-hint: "--project  [--from-file ] [--target {brief|plan|review|auto}] [--reason ] [--profile ] [--gates ]"
-model: opus
-allowed-tools: Read, Write, Edit, Bash, Grep, Glob
----
-
-# Ultrarevise Local v1.0
-
-Apply operator annotations from the voyage playground back into the source
-artifact (`brief.md`, `plan.md`, or `review.md`) with audit-trail in
-frontmatter. Implements **Handover 8 (annotation → revision)**.
-
-Pipeline position:
-
-```
-/trekbrief     →  brief.md
-/trekresearch  →  research/*.md
-/trekplan      →  plan.md
-/trekexecute   →  progress.json (+ commits)
-/trekreview    →  review.md
-/trekrevise    →  in-place revision of brief|plan|review     (this command)
-```
-
-The annotation source is the **operator-edited markdown** that the voyage
-playground exports. The playground reads `{target}.md`, lets the operator
-add anchored comments via three creation gestures, and exports a markdown
-blob carrying `` block-level HTML comments
-inline plus the operator's revised body content. `/trekrevise` consumes
-that blob, validates the anchor placement, computes a canonical
-`annotation_digest`, and replaces the source artifact in-place. A
-pre-revision backup (`{target}.md.local.bak`) is created and used to roll
-back if the post-write validator rejects the new content.
-
-The revision is **additive by default**: applying the same annotation set
-twice is idempotent (same digest, same body). Non-additive revisions
-(structural step reorder, section removal, frontmatter `*_version`
-changes) require an explicit `--reason ""`.
-
-See `docs/HANDOVER-CONTRACTS.md § Handover 8` for the schema contract.
-
-## Phase 1 — Parse mode and validate input
-
-Parse `$ARGUMENTS` via the shared arg-parser:
-
-```bash
-node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekrevise "$@"
-```
-
-The parser recognizes these flags (see `lib/parsers/arg-parser.mjs`
-FLAG_SCHEMA `trekrevise` entry):
-
-| Flag | Type | Purpose |
-|------|------|---------|
-| `--project ` | valued | Required. Path to the trekplan project folder containing the target artifact. |
-| `--from-file ` | valued | Optional. Read annotated content from a file instead of stdin/pasted prompt. |
-| `--target {brief\|plan\|review\|auto}` | valued | Optional. Which artifact to revise. `auto` (default) infers from frontmatter `type:` field of the annotated input. |
-| `--reason ` | valued | Optional. Required when revision is non-additive (see Phase 4). |
-| `--profile ` | valued | Optional. Model profile (`economy`, `balanced`, `premium`, custom). Default: `balanced`. |
-| `--gates ` | valued | Optional. Autonomy mode (`open`, `closed`, `adaptive`). Default: `adaptive`. |
-
-Resolution:
-
-1. If `--project` is missing, print usage and stop:
-   ```
-   Error: --project  is required.
-   Usage: /trekrevise --project  [--from-file ] [--target {brief|plan|review|auto}] [--reason ]
-   ```
-2. Trim trailing slash from `{dir}`. Set:
-   - `project_dir = {dir}`
-   - `brief_path = {dir}/brief.md`
-   - `plan_path = {dir}/plan.md`
-   - `review_path = {dir}/review.md`
-3. If `{dir}` does not exist:
-   ```
-   Error: project directory missing: {dir}
-   Run /trekbrief first.
-   ```
-4. Determine the **annotated input source**:
-   - If `--from-file ` is set, read that file. If the file is missing
-     or unreadable, stop with `Error: --from-file path not readable: {path}`.
-   - Otherwise, the annotated content MUST appear in the operator's prompt
-     (pasted directly into the chat). Capture it from the prompt text.
-   - If neither source yields content, stop with:
-     ```
-     Error: no annotated content provided. Pass --from-file  or paste the
-     content directly into your prompt.
-     ```
-5. **Reject multi-artifact bundle.** Scan the annotated input for two or
-   more lines matching `^---$` at line-start that introduce frontmatter
-   blocks (i.e. more than one `---\n...\n---\n` pair). If detected:
-   ```
-   Error: MULTI_ARTIFACT_NOT_SUPPORTED — annotated input contains more than
-   one artifact (frontmatter blocks detected: {N}). /trekrevise revises one
-   artifact per invocation. Split the bundle and re-run for each target.
-   ```
-   Stop. No partial revisions.
-6. Resolve `target`:
-   - If `--target` is `brief`, `plan`, or `review`: use it explicitly.
-   - If `--target` is `auto` (or missing): parse the annotated input's
-     frontmatter via `parseDocument`. Read the `type:` field — expect
-     `brief`, `plan`, or `review`. If absent, fall back to the artifact
-     whose schema validates against the input (try in order: brief, plan,
-     review). If none match, stop with:
-     ```
-     Error: cannot infer --target from annotated input frontmatter.
-     Pass --target {brief|plan|review} explicitly.
-     ```
-7. Resolve the target file path: `target_path = {project_dir}/{target}.md`.
-   If it does not exist, stop:
-   ```
-   Error: target artifact missing: {target_path}
-   Run /trek{brief|plan|review} first to produce it.
-   ```
-
-Set:
-- `mode = revise` (the only mode currently)
-- `profile`, `gates`, `reason` per flags
-
-Report:
-```
-Mode: revise
-Project: {project_dir}
-Target: {target} → {target_path}
-Source: {--from-file path | pasted}
-Profile: {profile}
-```
-
-## Phase 2 — Read source artifact + check rollback hygiene
-
-Stale-backup precheck:
-
-```bash
-test -f "{target_path}.local.bak"
-```
-
-If a `.local.bak` exists from a previously aborted run, abort:
-
-```
-Error: stale rollback backup found at {target_path}.local.bak
-Inspect it (it represents the pre-revision state of a prior aborted run),
-then either restore it manually with:
-  cp "{target_path}.local.bak" "{target_path}"
-  rm "{target_path}.local.bak"
-or delete it if you want to keep the current target_path:
-  rm "{target_path}.local.bak"
-After resolving, re-run /trekrevise.
-```
-
-Stop. The revision is not applied.
-
-Read the source artifact:
-
-```js
-import { parseDocument } from '${CLAUDE_PLUGIN_ROOT}/lib/util/frontmatter.mjs';
-const source = parseDocument(readFileSync(target_path, 'utf-8'));
-```
-
-If `source.valid === false`:
-
-```
-Error: source artifact has malformed frontmatter: {target_path}
-Fix it manually before annotating.
-```
-
-Capture:
-- `existing_frontmatter = source.parsed.frontmatter`
-- `existing_body = source.parsed.body`
-- `existing_revision = existing_frontmatter.revision || 0`
-
-## Phase 3 — Parse anchors + validate placement
-
-Parse anchors from the **annotated input** (not the source artifact):
-
-```js
-import { parseAnchors, validateAnchorPlacement, stripAnchors } from '${CLAUDE_PLUGIN_ROOT}/lib/parsers/anchor-parser.mjs';
-const annotated = parseDocument(annotated_input_text);
-const annotated_body = annotated.parsed.body;
-const anchors_result = parseAnchors(annotated_body);
-```
-
-If `anchors_result.valid === false`:
-
-```
-Error: anchor parse failed in annotated input.
-{for each error: file:line:rule_key — message}
-```
-
-Stop. **No partial revisions** — abort BEFORE any write.
-
-Run placement validation:
-
-```js
-const placement_result = validateAnchorPlacement(annotated_body, anchors_result.parsed);
-```
-
-If `placement_result.valid === false`:
-
-```
-Error: anchor placement violations detected.
-{for each error: line N: rule_key — message}
-```
-
-Stop.
-
-Capture `anchors = anchors_result.parsed` (an array of anchor objects with
-`{id, target, line, snippet?, intent?}`).
-
-If `anchors.length === 0`, the operator submitted an annotation-free
-revision. Continue — empty-anchor round-trip is a valid case (used by
-SC2). The body diff will still be applied; the audit fields will record
-zero anchors plus an all-zeros digest baseline.
-
-## Phase 4 — Compute revision diff + digest
-
-Compute the canonical digest from the annotation set:
-
-```js
-import { computeAnnotationDigest } from '${CLAUDE_PLUGIN_ROOT}/lib/parsers/annotation-digest.mjs';
-const annotation_digest = computeAnnotationDigest(anchors);
-```
-
-Determine the new revision counter:
-
-```js
-const new_revision = (existing_revision | 0) + 1;
-```
-
-**Detect non-additive revision.** A revision is non-additive when ANY of:
-
-- The body removes a `### Step N:` heading that previously existed.
-- The body removes any `## ` heading per the target's
-  validator (e.g. plan: "Implementation Plan", "Verification"; brief:
-  "Goal", "Success Criteria"; review: "Executive Summary", "Coverage").
-- The frontmatter changes any `*_version` field (e.g. `plan_version`,
-  `brief_version`, `review_version`).
-- A `### Step N:` heading is reordered (different N sequence than before).
-
-Compute these by comparing `existing_body` heading sequence with the
-`annotated_body` (with anchors stripped via `stripAnchors`). The
-heading-sequence comparison is sufficient — content edits inside an
-existing step are always additive.
-
-If non-additive AND `--reason` was NOT supplied:
-
-```
-Error: non-additive revision detected:
-  - {bullet list of detected non-additive changes}
-
-Re-run with --reason "" to acknowledge the structural
-change and record it in revision_reason for the audit trail.
-```
-
-Stop. The revision is not applied.
-
-If non-additive AND `--reason` is supplied: capture
-`revision_reason = `. If additive, leave `revision_reason`
-unset (omitted from frontmatter).
-
-## Phase 5 — Apply revisions in-place
-
-Strip anchors from the annotated body to produce the new artifact body:
-
-```js
-const new_body = stripAnchors(annotated_body);
-```
-
-Build the new frontmatter object by merging the existing fields with the
-revision audit fields:
-
-```js
-const new_frontmatter = {
-  ...existing_frontmatter,
-  revision: new_revision,
-  source_annotations: anchors.map(a => ({
-    id: a.id,
-    target_artifact: target,
-    target_anchor: a.target,
-    line: a.line,
-    intent: a.intent || 'change',
-    snippet: a.snippet || '',
-    timestamp: new Date().toISOString(),
-  })),
-  annotation_digest,
-  ...(revision_reason ? { revision_reason } : {}),
-};
-```
-
-Apply the revision via `revisionGuard` from `lib/util/revision-guard.mjs`,
-which performs the full **backup → mutate → atomic write → validate →
-rollback-on-fail** orchestration:
-
-```js
-import { revisionGuard } from '${CLAUDE_PLUGIN_ROOT}/lib/util/revision-guard.mjs';
-import { validateBrief, validatePlan, validateReview } from '${CLAUDE_PLUGIN_ROOT}/lib/validators/...';
-
-const validator = ({
-  brief: validateBrief,
-  plan: validatePlan,
-  review: validateReview,
-})[target];
-
-const result = revisionGuard(
-  target_path,
-  ({ frontmatter, body }) => ({
-    frontmatter: new_frontmatter,
-    body: new_body,
-  }),
-  validator,
-);
-```
-
-`revisionGuard` returns `{outcome, validator_result, sha256_before,
-sha256_after, error?}`. Outcomes:
-
-- `applied` — write succeeded, validator passed, `.local.bak` deleted.
-- `rolled-back` — write succeeded, validator FAILED, file restored from
-  `.local.bak`, byte-identical to pre-revision state.
-- `mutator-failed` — pre-existing backup, mutator threw, or read failed.
-
-## Phase 6 — Validate revision
-
-Branch on the `revisionGuard` outcome:
-
-### outcome === 'applied'
-
-The revision is in place. Surface validator warnings (if any) to the
-operator, but do not roll back. Continue to Phase 7.
-
-### outcome === 'rolled-back'
-
-The post-write validator rejected the new content. The file is
-byte-identical to its pre-revision state.
-
-```
-Validator REJECTED the revision. Rolled back to pre-revision state.
-sha256_before === sha256_after (byte-identical): {true|false}
-
-Validator errors:
-  - {code} {file}:{line} — {message}
-  ...
-
-To inspect the proposed (rejected) revision, retry with --from-file pointing
-at a fixed annotated input, or fix the annotation set in the playground and
-re-export.
-```
-
-Stop. The audit-trail fields are NOT recorded; revision counter is
-unchanged.
-
-### outcome === 'mutator-failed'
-
-```
-Error: revision could not be applied: {revisionGuard.error}
-
-If the error references a pre-existing backup, follow Phase 2's
-remediation steps.
-```
-
-Stop.
-
-## Phase 7 — Optional review-gate (plan/review targets only)
-
-Only runs when `target === 'plan'` AND `{review_path}` exists, OR
-`target === 'review'` AND `{plan_path}` exists. Otherwise skip to Phase 8.
-
-Run the existing review (validate-only mode) against the revised plan, or
-re-validate the revised review:
-
-```bash
-node ${CLAUDE_PLUGIN_ROOT}/lib/validators/review-validator.mjs --json "{review_path}"
-```
-
-If the validator returns BLOCK or FAIL:
-
-```
-Review-gate WARN: revised {target}.md is in place, but the review at
-{review_path} contains BLOCKER findings against the now-revised content.
-Verdict: BLOCK
-Top findings:
-  - {list}
-
-The revised file is NOT auto-rolled-back. You may:
-  - Re-annotate to address the BLOCKER findings, then /trekrevise again.
-  - Accept the gap and run /trekreview to refresh review.md.
-  - Manually restore via:
-      cp "{target_path}.local.bak" "{target_path}"
-    (Note: the .local.bak was deleted on Phase 6 success. To re-rollback
-    after Phase 7, use git: `git checkout HEAD -- "{target_path}"` if you
-    have not yet committed the revision.)
-```
-
-The revised file remains as written. Continue to Phase 8.
-
-If the gate passes (verdict ALLOW or WARN-only): continue silently.
-
-## Phase 8 — Stats + report
-
-Append a stats line to `${CLAUDE_PLUGIN_DATA}/trekrevise-stats.jsonl`
-(create the file if it does not exist):
-
-```json
-{"ts":"{ISO-8601}","target":"{brief|plan|review}","project_dir":"{dir}","revision":{N},"anchor_count":{N},"digest":"{16-char-hex}","validator_verdict":"{pass|fail-rolled-back}","outcome":"{applied|rolled-back|mutator-failed}","profile_used":"{profile}"}
-```
-
-Use inline `appendFileSync` (mirrors the jsonl-append pattern in
-`hooks/scripts/post-bash-stats.mjs:50`). If `${CLAUDE_PLUGIN_DATA}` is
-unset or not writable, skip stats silently. Never let stats failures block
-the main workflow.
-
-Emit the human-readable summary:
-
-```
-## Ultrarevise Complete
-
-**Project:** {project_dir}
-**Target:** {target} ({target_path})
-**Revision:** {existing_revision} → {new_revision}
-**Anchors applied:** {N}
-**annotation_digest:** {16-char-hex}
-**Outcome:** {applied | rolled-back | mutator-failed}
-{if revision_reason}: **revision_reason:** {revision_reason}
-
-### Audit fields written
-- revision: {N}
-- source_annotations: {N entries}
-- annotation_digest: {hex}
-{if revision_reason}: - revision_reason: "{reason}"
-
-{if Phase 7 surfaced findings}:
-### Review-gate WARN
-- Verdict: {BLOCK|WARN}
-- Top findings:
-  - {list}
-
-You can:
-- Inspect the revised file at {target_path}
-- Run /trekreview --project {project_dir} to refresh review.md
-- Run /trekplan --project {project_dir} to extend the plan
-- Re-annotate via the playground and run /trekrevise again
-- Roll back manually via: git checkout HEAD -- "{target_path}"
-```
-
-## Profile (v4.1)
-
-Accepts `--profile ` where `` is `economy`, `balanced`, `premium`,
-or a custom profile under `voyage-profiles/`. Default: `balanced`.
-
-Resolution order (per `lib/profiles/resolver.mjs`):
-
-1. `--profile` flag (source: `flag`)
-2. `VOYAGE_PROFILE` env-var (source: `env`)
-3. `balanced` default (source: `default`)
-
-The selected profile drives `phase_models.revise`. `/trekrevise` is mostly
-deterministic (parsing + validating + atomic writes), so all built-in
-profiles use sonnet for any sub-agent invocation. The operator-facing
-synthesis in Phase 6/7 stays in the main thread regardless of profile.
-
-Examples:
-
-```
-/trekrevise --profile balanced --project .claude/projects/2026-05-09-foo
-VOYAGE_PROFILE=premium /trekrevise --project ... --target plan
-```
-
-Stats records emit `profile_used` (and `profile_source` when available).
-
-## Hard rules
-
-- **No partial revisions.** If anchor parse or placement validation fails
-  in Phase 3, abort BEFORE any write. The source artifact is never left
-  in a half-revised state.
-- **One artifact per invocation.** Multi-artifact bundles return
-  `MULTI_ARTIFACT_NOT_SUPPORTED`. Split into per-artifact runs.
-- **revision_reason is required for non-additive changes.** Step
-  reorder, section removal, and `*_version` bumps require an explicit
-  reason recorded in frontmatter for audit.
-- **Backup hygiene.** Pre-existing `.local.bak` blocks the run. Operator
-  must inspect or delete it; the executor never auto-overwrites it.
-- **Validator is the gate.** A revision that writes successfully but
-  fails post-write validation is rolled back to byte-identical
-  pre-revision state. Audit fields are NOT recorded on rollback.
-- **Idempotent digest.** Re-applying the same annotation set yields the
-  same `annotation_digest`. The digest is canonical SHA-256 over a
-  field-sorted, id-sorted, pipe-separated, line-joined serialization
-  (16-char hex prefix).
-- **Forward-compat fields.** `revision`, `source_annotations`,
-  `annotation_digest`, and `revision_reason` are additive frontmatter
-  fields. Validators that predate v4.2 ignore them. Artifacts without
-  `revision:` are treated as `revision: 0`.
-- **Anchor format.** ``
-  block-level only; placement disipline enforced (not in list-items, not
-  inside fenced code blocks, not at line-start collisions with frontmatter
-  delimiter, manifest:, plan_version:, ### Step N:, ## required sections,
-  or 40-char hex finding-IDs).
-- **No production code.** This command never runs production code, never
-  writes to anything outside `{project_dir}` and `${CLAUDE_PLUGIN_DATA}`.
-- **Operator has final say.** The review-gate in Phase 7 is advisory —
-  it never auto-rolls-back a successful revision. The operator decides
-  whether to re-annotate, refresh the review, or accept the gap.
diff --git a/plugins/voyage/docs/HANDOVER-CONTRACTS.md b/plugins/voyage/docs/HANDOVER-CONTRACTS.md
index efec932..a45eb05 100644
--- a/plugins/voyage/docs/HANDOVER-CONTRACTS.md
+++ b/plugins/voyage/docs/HANDOVER-CONTRACTS.md
@@ -1,6 +1,6 @@
 # Handover Contracts (voyage-suite local pipeline)
 
-This document is the single source of truth for the file formats that pass between the four commands of the `trekplan` pipeline. When you fork the plugin or extend a stage, the contracts below tell you what every producer must write and what every consumer is allowed to assume.
+This document is the single source of truth for the file formats that pass between the commands of the `trekplan` pipeline. There are seven handovers. When you fork the plugin or extend a stage, the contracts below tell you what every producer must write and what every consumer is allowed to assume.
 
 For each handover, the same headings appear in the same order: **Producer**, **Consumer**, **Path conventions**, **Frontmatter schema**, **Body invariants**, **Validation strategy**, **Versioning**, **Failure modes**.
 
@@ -16,7 +16,6 @@ Each artifact carries an explicit version field. Schema bumps are coordinated:
 | `progress.json` | `schema_version` (top-level) | `"1"` |
 | `review.md` | `review_version` (frontmatter) | `1.0` |
 | `.session-state.local.json` | `schema_version` (top-level) | `1` (number) |
-| `brief.md` / `plan.md` / `review.md` (annotated) | `revision` (frontmatter) | `0` (implicit), `1+` after `/trekrevise` |
 
 ## Breaking-change protocol
 
@@ -37,7 +36,6 @@ Each artifact carries an explicit version field. Schema bumps are coordinated:
 | 5. progress.json (resume) | `lib/validators/progress-validator.mjs` |
 | 6. review → plan | `lib/validators/review-validator.mjs` |
 | 7. session-state (multi-session resume) | `lib/validators/session-state-validator.mjs` |
-| 8. annotation → revision | `lib/parsers/anchor-parser.mjs` + `lib/parsers/annotation-digest.mjs` (parsing) and the existing brief / plan / review validators (forward-compat — additive fields tolerated) |
 
 Every validator exposes a CLI: `node lib/validators/.mjs --json ` returns `{valid, errors[], warnings[], parsed}`. Errors and warnings have stable `code` fields for downstream tooling.
 
@@ -440,96 +438,6 @@ The `next-session-prompt-validator` (`lib/validators/next-session-prompt-validat
 
 ---
 
-## Handover 8 — annotation → revision
-
-**Handover 8 closes the operator-feedback loop.** Where Handovers 1–4 flow forward (brief → research → plan → execute), Handover 5 makes execute resumable, Handover 6 routes review findings back into planning, and Handover 7 makes multi-session work survivable across fresh chats, **Handover 8 lets a human operator annotate an artifact (brief, plan, or review) inside the playground and feed those annotations back into a revision cycle without losing the original artifact's byte-for-byte content**. The pipeline becomes round-trippable: a single artifact can be annotated, revised, and re-rendered repeatedly, each revision recorded with a deterministic digest in frontmatter.
-
-**Producer:**
-- The operator (manual step) — open the artifact in `playground/voyage-playground.html`, drag-select or hover-to-anchor, fill comment + intent in the modal, click "Eksporter batch" to copy the `/trekrevise` invocation to clipboard.
-- `/trekrevise --project ` (Phase 3 — apply) — consumes the pasted batch, writes anchor comments back into the target artifact, increments `revision:`, appends entries to `source_annotations:`, and computes a fresh `annotation_digest`.
-
-**Consumer:**
-- Subsequent `/trekplan --project ` if the revised brief or plan needs further re-planning.
-- Subsequent `/trekexecute --project ` if the revised plan is ready for execution.
-- All existing validators (brief, plan, review) — they tolerate the additive frontmatter fields without version bumps.
-
-**Path conventions:**
-- The annotated artifact is the same canonical file in `{project_dir}/` (`brief.md`, `plan.md`, `review.md`). Revisions are *in-place* — no `plan-revN.md` shadow files. Audit trail lives in git commits + the `revision:` counter + `source_annotations:` list inside frontmatter.
-- The playground itself lives at `playground/voyage-playground.html` (single self-contained file with vendored `markdown-it` + `highlight.js` under `playground/lib/`).
-
-**Frontmatter schema (additive — applies to brief.md, plan.md, review.md):**
-
-| Field | Type | Required | Allowed values | Notes |
-|---|---|---|---|---|
-| `revision` | number | optional | `0`, `1`, `2`, … | Absent = `0` (forward-compat). Incremented by `/trekrevise` on each apply |
-| `source_annotations` | list | optional | block-style YAML list of dicts | Absent = empty. Audit trail of every annotation that has been folded into this artifact |
-| `annotation_digest` | string | optional | first 16 hex chars of canonical SHA-256 over the sorted `source_annotations` array | Absent = no annotations applied yet. Deterministic — re-applying the same set yields the same digest |
-| `revision_reason` | string | optional | free-form text | **Required only** when the revision is non-additive (e.g. removed scope, replaced an SC). Operators are encouraged to fill it for any revision |
-
-Each entry in `source_annotations` is a YAML dict:
-
-```yaml
-source_annotations:
-  - id: ANN-0001
-    target_artifact: plan.md
-    target_anchor: step-3
-    intent: change          # change | add | remove | clarify | risk
-    comment: "consider rolling back if cache_creation jumps >10%"
-    line: 412
-```
-
-**Anchor format (block-level HTML comments inside the body):**
-
-```html
-
-```
-
-Anchors are placed **only at block boundaries** — not in list items, not mid-paragraph, not at line-start collision points where a markdown parser might fold them into surrounding content. The playground enforces this discipline via `validateAnchorPlacement()` in `lib/parsers/anchor-parser.mjs`. Anchors survive markdown rendering as comments (no visible artifact) and round-trip through `parseAnchors → addAnchors → stripAnchors` byte-identically (SC2 contract).
-
-**Body invariants:**
-- The artifact's existing required sections remain untouched. `/trekrevise` writes anchor comments only at block boundaries declared by the operator's annotations.
-- After each apply: byte-identical content outside anchor blocks. Stripping all anchors with `stripAnchors()` MUST yield the artifact's original body modulo the operator's deliberate prose edits.
-
-**Validation strategy:**
-
-| Layer | When | What |
-|---|---|---|
-| `revision` shape | every read | Number (or absent → treat as `0`). Negative or non-integer rejected by validator |
-| `source_annotations` shape | every read | Array of dicts; each dict must include `id`, `target_artifact`, `target_anchor`, `intent`. Other fields tolerated (drift-WARN) |
-| `annotation_digest` shape | every read | 16-char lowercase hex; presence requires `source_annotations` non-empty |
-| Anchor placement | `/trekrevise` Phase 2 (validate) | `validateAnchorPlacement()` rejects in-list / mid-paragraph / line-start anchors before write |
-| Round-trip integrity | `/trekrevise` Phase 4 (post-write) | `stripAnchors()` of the just-written file MUST equal the pre-write body |
-
-The parsers (`lib/parsers/anchor-parser.mjs`, `lib/parsers/annotation-digest.mjs`) are pure functions — no I/O, fully unit-tested. The existing brief / plan / review validators in `lib/validators/` already tolerate the new optional fields (forward-compat — see Step 12 manifest pins).
-
-**Forward-compat — additive principle:** Artifacts without any of the four new fields validate as `revision: 0` with empty annotations. This means every brief / plan / review written before v4.2 remains valid without migration. Producers (operators using `/trekrevise`) opt into the schema by writing the fields; consumers tolerate their presence or absence equally.
-
-**Idempotence:** Re-applying the same annotation batch on the same artifact (same anchors, same intents, same comments) yields the same `annotation_digest` and no body diff outside anchor refresh. This is enforced by `computeAnnotationDigest()` canonicalizing the sorted `source_annotations` array before hashing — operators can replay a batch safely, and the test suite pins this with `tests/parsers/annotation-digest.test.mjs`.
-
-**Versioning:** No `*_version` bump for v4.2 — the four new fields are additive. A future schema break (e.g. removing `target_anchor` in favor of structured pointer) would bump `brief_version` / `plan_version` / `review_version` per the breaking-change protocol.
-
-**Failure modes:**
-- `ANNOTATION_PLACEMENT_INVALID` → `/trekrevise` Phase 2 halts; operator re-anchors via playground
-- `ANNOTATION_DIGEST_DRIFT` → digest computed at apply-time differs from operator's expected digest in the pasted batch; halt with mismatch report (suggests the batch was hand-edited after export)
-- `ANNOTATION_ROUNDTRIP_FAIL` → post-write `stripAnchors()` does not yield the original body; rollback restores the byte-identical pre-write file from `*.local.bak`
-- Parser failures from `parseAnchors()` produce `{valid: false, errors: [...]}` and `/trekrevise` halts before any write. The atomic-write pattern (tmp-file + rename) guarantees the canonical file is never partially updated.
-
-### § Lifecycle
-
-The annotation cycle has three stages — no persistent state file (unlike Handover 7), only the artifact frontmatter and git history record what happened:
-
-| Stage | Owner | Action |
-|---|---|---|
-| Annotate | Operator + playground UI | Open artifact in `playground/voyage-playground.html`, anchor + comment, click "Eksporter batch" → clipboard contains a `/trekrevise --project  --apply '{...JSON...}'` command |
-| Apply | `/trekrevise` | Phase 1 parse the pasted batch, Phase 2 validate placement, Phase 3 atomically write anchors + frontmatter (`revision: N+1`, append to `source_annotations`, recompute `annotation_digest`), Phase 4 round-trip integrity check, Phase 5 commit |
-| Re-render | Playground (next open) | The freshly-revised artifact loads with anchors visible as inline comment markers; operator can iterate or call `/trekplan` / `/trekexecute` on the revised file |
-
-**Single-iteration MVP (v4.2 scope):** Each operator annotation batch produces one `revision:` increment. Multi-iteration loops (e.g. revise → re-review → revise again without operator intervention) are deferred indefinitely — the brief's SC4 wording is single-revision, and operator validation that multi-iteration is desired has not been collected (see plan Alternatives table). The single-iteration MVP keeps the audit trail unambiguous: one `revision:` bump per `/trekrevise` invocation.
-
-**Stale-anchor principle:** Unlike `.session-state.local.json`, anchors are durable and intentionally retained — they document *why* a revision happened. There is no `--cleanup` for anchors. Removing them is a manual operator decision, executed via the playground "Strip all anchors" affordance or hand-editing the source. `/trekrevise` does not remove anchors automatically.
-
----
-
 ## Stability summary
 
 | Handover | Validation strength | Owner | Risk |
@@ -541,6 +449,5 @@ The annotation cycle has three stages — no persistent state file (unlike Hando
 | 5. progress.json | shape + resume readiness | this plugin | medium — drift during compaction handled by pre-compact-flush hook (CC v2.1.105+) |
 | 6. review → plan | strict at write, soft at read | this plugin | low — additive feedback loop; consumer falls back gracefully when source_findings is absent |
 | 7. session-state (multi-session resume) | required-fields + status enum + drift-WARN extras | this plugin | low — readers tolerate unknown keys; writers are owned by trekexecute Phase 8 + helper command |
-| 8. annotation → revision | parser-strict at write (placement + round-trip), validator-soft at read (additive fields) | this plugin | low — additive frontmatter; existing artifacts validate as `revision: 0` without migration |
 
 When extending the plugin or adding a new pipeline stage, follow the same pattern: produce an artifact with a versioned frontmatter (or `schema_version` for JSON), write a validator under `lib/validators/`, add fixtures under `tests/fixtures/`, and add an entry to this document.
diff --git a/plugins/voyage/docs/annotation-quickstart.md b/plugins/voyage/docs/annotation-quickstart.md
deleted file mode 100644
index cb314ac..0000000
--- a/plugins/voyage/docs/annotation-quickstart.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# Annotation playground — quickstart
-
-The `/trekrevise` command and the `playground/voyage-playground.html` page
-let you annotate any voyage artifact (`brief.md`, `plan.md`, or `review.md`)
-and fold the annotations back in-place with a deterministic audit trail.
-
-This is **Handover 8** in `docs/HANDOVER-CONTRACTS.md`. For schema details,
-read that document first.
-
-## Hands-on with the example fixture
-
-The plugin ships a canonical fixture at
-`tests/fixtures/annotation/annotation-example.md` that is the same shape an
-operator would annotate. Use it to verify your playground works before
-touching a real project.
-
-## Seven steps from artifact to revised file
-
-1. **Open the playground.** Open `plugins/voyage/playground/voyage-playground.html`
-   in any modern browser. No build, no server, no network calls — the page
-   ships vendored `markdown-it` and `highlight.js` under `playground/lib/`.
-
-2. **Paste the artifact content.** Copy the full body of your `brief.md`,
-   `plan.md`, or `review.md` (including frontmatter) into the textarea on
-   the left. The right pane renders the markdown live. For larger artifacts
-   you can also generate the rendered HTML offline with
-   `node plugins/voyage/scripts/render-artifact.mjs  --out /tmp/x.html`.
-
-3. **Anchor a comment.** Drag-select text inside a paragraph, or hover a
-   block-level element and click the anchor button that appears. The
-   playground refuses anchors inside list items, mid-paragraph, or at
-   line-start collision points — only block boundaries are valid (per the
-   placement discipline in `lib/parsers/anchor-parser.mjs`).
-
-4. **Fill the modal.** Choose an intent (`change`, `add`, `remove`,
-   `clarify`, or `risk`), write your comment, save. Repeat for every
-   anchor you want in this batch. The sidebar shows your batch growing as
-   a critique-card-list.
-
-5. **Export the batch.** Click "Eksporter batch". The playground copies a
-   complete `/trekrevise --project  --apply '{...JSON...}'` invocation
-   to your clipboard. The JSON encodes every anchor, intent, and comment.
-
-6. **Apply via `/trekrevise`.** Paste the command in your Claude Code chat.
-   The command parses + validates the batch, atomically writes anchor
-   comment blocks back into the source artifact, increments `revision:`,
-   appends entries to `source_annotations:`, and recomputes
-   `annotation_digest`. Body content outside anchor blocks remains
-   byte-identical.
-
-7. **Verify and iterate.** Re-open the revised file in the playground to
-   see anchors as inline comment markers. If you want another revision
-   pass, repeat from step 3 — each batch produces one `revision:` bump.
-   Single-iteration MVP per research-05; multi-iteration loops are
-   deferred.
diff --git a/plugins/voyage/docs/sc1-checklist-verification.md b/plugins/voyage/docs/sc1-checklist-verification.md
deleted file mode 100644
index 4910e19..0000000
--- a/plugins/voyage/docs/sc1-checklist-verification.md
+++ /dev/null
@@ -1,129 +0,0 @@
-# SC1 10-element Checklist Verification — voyage v4.3 playground
-
-**Verifisert:** 2026-05-10 (Sesjon 6, Wave 7)
-**Referanse:** `plugins/llm-security/playground/llm-security-playground.html` (visuell paritet)
-**Metode:** Static-grep (Group A tester, Step 28) + manuell side-by-side + Playwright pixel-diff (Step 30)
-**Status:** 8 av 10 PASS ved bokstavelig criteria, 2 redefinert per scope-guardian Assumptions (operatør-sign-off)
-
-## Sammendrag
-
-| # | Element | Status | Evidens |
-|---|---------|--------|---------|
-| 1 | Header + breadcrumb | PASS | Group A test SC1.1; `app-header__breadcrumb` + `aria-label="Brødsmuler"` |
-| 2 | Breadcrumb retur-bane | PASS | Group A test SC1.2; `breadcrumb-click` handler i `voyage-playground.html:1830` |
-| 3 | Theme bootstrap IIFE | PASS | Group A test SC1.3; `data-theme` + `prefers-color-scheme` IIFE i HTML head |
-| 4 | Onboarding-grid (redefinert) | PASS | Group A test SC1.4 + Group B fleet-grid CSS parity-test (99707f51) verifiserer at vendored DS `components-tier3-supplement.css` `.fleet-grid` block fortsatt har `grid-template-columns: repeat(4, 1fr)` + `gap: var(--space-3)`. Voyage bruker `fleet-grid` + `fleet-tile` istedenfor onboarding-grid (scope-guardian SC-GAP-1, Assumption #21). |
-| 5 | A11Y-panel | PASS | Group A test SC1.5; `guide-panel--info` + `key-stats` + `findings__items` (Wave 5 Step 22) |
-| 6 | Screenshots-spor | PASS | Group D test SC1.6; `loadProjectDirectory` leser `docs/screenshots/**/*.png` via `readAsDataURL` (2 MB cap) og `renderScreenshotGallery` mounter en `
`-grid i dashboardet. Erstatter scope-guardian SC-GAP-2 PASS-redef med faktisk inline gallery (v4.3 Step 8, finding 31d28f65). | -| 7 | Body typografi | PASS | Group A test SC1.7; `var(--font-size-*)` + `var(--font-family-mono)` brukt gjennomgående | -| 8 | Spacing rhythm | PASS | Group A test SC1.8; ≥5 distinkte `var(--space-N)` referanser | -| 9 | Color-token fidelity | PASS | Group A test SC1.9; `badge--scope-voyage` + `--color-scope-voyage` brukt | -| 10 | Dark-mode parity | PASS | Group A test SC1.10; `data-theme="dark"` default + persistens via `voyage-theme` localStorage | - -**Nettoresultat:** 10/10 verifisert (8 bokstavelig + 2 redefinert med operatør-sign-off i Assumptions #21 og #22). Ingen revisjons-loop til Wave 1-5 utløst. - -## Detaljer per element - -### Element 1 — Header med breadcrumb -- **Bokstavelig krav:** sticky topbar med breadcrumb-navigasjon -- **Implementering:** `class="app-header__breadcrumb"` + `aria-label="Brødsmuler"` i `renderTopbar` (Step 8) -- **Test:** `tests/playground/voyage-playground.test.mjs` — `SC1.1 header — app-shell topbar with breadcrumb` -- **Status:** PASS - -### Element 2 — Breadcrumb klikkbar retur-bane -- **Bokstavelig krav:** klikk på "Dashboard" i breadcrumb returnerer til hovedoversikt -- **Implementering:** `breadcrumb-click` handler renderer dashboard-fleet-grid via `renderDashboard()` -- **Test:** `SC1.2 breadcrumb — clickable returns to dashboard` -- **Status:** PASS - -### Element 3 — Theme bootstrap IIFE -- **Bokstavelig krav:** sync IIFE i `` setter `data-theme` før første paint, leser bruker-pref + OS-pref -- **Implementering:** Linje 22-26 i HTML; sjekker `localStorage('voyage-theme')` → `prefers-color-scheme: dark` matchMedia → fallback `dark` -- **Test:** `SC1.3 theme bootstrap — IIFE sets data-theme + colorScheme` -- **Status:** PASS - -### Element 4 — Onboarding-grid (REDEFINERT) -- **Bokstavelig krav:** llm-security har 4 onboarding-tiles (Quickstart / Latest scan / Threats / Roadmap) -- **Voyage-tolkning:** voyage-domain har én plugin uten onboarding-flow. Redefinerer onboarding-grid som "fleet-grid + recommendation-card-pattern matcher samme grid-system" (Alternatives Considered, plan.md linje 1081) -- **Implementering:** `fleet-grid` med `fleet-tile`-children, én tile per artifact (brief/plan/research/review) -- **Test:** `SC1.4 onboarding-grid equivalent — fleet-grid pattern` + Group B `SC1.4 fleet-grid CSS parity vs vendored DS (99707f51)` som verifiserer at `components-tier3-supplement.css` `.fleet-grid` block fortsatt har `grid-template-columns: repeat(4, 1fr)` + `gap: var(--space-3)` -- **Status:** PASS (fleet-grid CSS parity verifisert via Group B strukturell test mot vendored DS; scope-guardian SC-GAP-1 lukket via /trekreview Sesjon 13, Assumption #21 operatør-sign-off står) - -### Element 5 — A11Y-panel -- **Bokstavelig krav:** tilgjengelighets-panel viser severity-counters + findings-list -- **Implementering:** `guide-panel guide-panel--info` container med `key-stats` severity-grid + `findings__items` ordered list. `wireA11yToggle` kobler topbar-button til toggle. Built fra DS-primitives (Wave 5 Step 22) -- **Hooks:** `window.__voyage.scheduleRender({ a11yViolations })` populerer panelet fra Playwright-spec -- **Test:** `SC1.5 A11Y panel — guide-panel--info + key-stats + findings` -- **Status:** PASS - -### Element 6 — Screenshots-spor -- **Bokstavelig krav:** llm-security har inline gallery med thumbnail-grid for case-studies -- **Voyage-implementering (v4.3 Step 8, finding 31d28f65):** `loadProjectDirectory` detekterer `docs/screenshots/**/*.png` via path-prefix-match, leser dem via `FileReader.readAsDataURL()` med 2 MB per-bilde cap (overskridelse annonseres via aria-live). `renderScreenshotGallery(screenshots)` bygger en `
`-grid med `<filename>` som mountes under `fleet-grid` i `renderDashboard`. Erstatter den tidligere PASS-redef-tolkningen — voyage har nå et faktisk inline gallery. -- **Beholder:** `window.__voyage` hooks fra Wave 5 Step 23 (`navigate`, `scheduleRender`, `getProjectArtifacts`) + `docs/screenshots/README.md` mappe-konvensjon — fortsatt brukt av Playwright-spec og av operatør for manuell screenshot-prosedyre. -- **Test:** Group D Playwright-test `SC1.6 inline gallery — data:image PNGs rendered (31d28f65)` injiserer en fixture-artifact via `scheduleRender` og asserter `#voyage-dashboard img[src^="data:image/png"]` count > 0. -- **Status:** PASS (inline gallery implementert; scope-guardian SC-GAP-2 lukket). - -### Element 7 — Body typografi -- **Bokstavelig krav:** typografi-skala bruker DS-tokens, ikke literal pixel-verdier -- **Implementering:** `var(--font-size-{xs,sm,md,lg,xl})` + `var(--font-family-mono)` brukt gjennomgående. Wave 1 Step 5 fjernet literal pixel font-sizes. -- **Test:** `SC1.7 body typography — DS font-size + family tokens` -- **Note:** En enkelt `font-size: 0.7rem` literal eksisterer i HTML (linje ~361) — dokumentert i Wave 1; ikke blocker. -- **Status:** PASS - -### Element 8 — Spacing rhythm -- **Bokstavelig krav:** ≥5 distinkte `--space-N` token-referanser for konsistent spacing -- **Implementering:** Verifisert via grep-count i Group A test -- **Test:** `SC1.8 spacing rhythm — DS --space-N tokens used` -- **Status:** PASS - -### Element 9 — Color-token fidelity -- **Bokstavelig krav:** voyage-scope tokens definert + brukt -- **Implementering:** Wave 0 Step 1 la til `--color-scope-voyage` + `badge--scope-voyage` i DS `base.css`. Brukt i playground via `class="badge badge--scope-voyage"` i topbar. -- **Test:** `SC1.9 color-token fidelity — voyage-scope tokens + DS colors` -- **Status:** PASS - -### Element 10 — Dark-mode parity -- **Bokstavelig krav:** dark mode er førsteklasses borger (ikke lag-på), default-teme følger system-pref -- **Implementering:** `` som default; bootstrap-IIFE respekterer `voyage-theme` localStorage, deretter `prefers-color-scheme: dark`, deretter fallback `dark` -- **Test:** `SC1.10 dark-mode parity — explicit dark default + bootstrap` -- **Pixel-diff baseline:** `tests/e2e/snapshots/voyage-playground-dark.png` (Step 30 spec) -- **Status:** PASS - -## Baseline-screenshots (Playwright) - -Etablert via `npx playwright test --update-snapshots` 2026-05-10. Lagret under -`tests/e2e/snapshots/`: - -- `voyage-playground-light.png` (1280×900, light theme) -- `voyage-playground-dark.png` (1280×900, dark theme) - -Pixel-diff-spec (`tests/e2e/voyage-playground-a11y.spec.mjs`) sammenlikner mot -disse baselines med `maxDiffPixelRatio: 0.02`. Avvik > 2% utløser test-fail. - -## Kjente WCAG-violations (baseline'd, defer til v4.4) - -Wave 7 = VERIFICATION ONLY. HTML er FROZEN i Sesjon 6 — ingen fix. Existing -critical/serious violations er recorded i `tests/e2e/snapshots/a11y-baseline.json` -som delta-baseline: - -```json -{ - "light": { "aria-hidden-focus": 1, "color-contrast": 4 }, - "dark": { "aria-hidden-focus": 1 } -} -``` - -- **`aria-hidden-focus`** (1 forekomst, light + dark): aria-hidden element inneholder fokuserbart innhold -- **`color-contrast`** (4 forekomster, kun light theme): `.key-stat--critical > .key-stat__label` har kontrast 3.85:1 (WCAG AA krever 4.5:1) - -**Defer-rationale:** v4.3 brief Wave 7 mandat er `test_strategy.coverage`-utvidelse, ikke -WCAG-fixing. v4.4 skal unfreeze HTML og adressere disse via DS-token-justering. - -## Verifikasjon-sjekkliste (Steg-for-steg) - -- [x] Group A static-grep tester (Step 28) passerer alle 17 SC1-elementer -- [x] Manuell side-by-side mot llm-security-playground (operatør, dette dokument) -- [x] Playwright PNG-baselines etablert for light + dark theme -- [x] axe-core delta-baseline JSON committed -- [x] Pixel-diff spec passerer 2% terskel -- [x] WCAG-violations dokumentert som baseline (defer til v4.4) diff --git a/plugins/voyage/docs/screenshots/README.md b/plugins/voyage/docs/screenshots/README.md deleted file mode 100644 index 96eac76..0000000 --- a/plugins/voyage/docs/screenshots/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# Voyage playground screenshot convention (v4.3) - -This directory holds reference screenshots for the Voyage playground -(`playground/voyage-playground.html`). They serve as a visual baseline for -manual review of design changes and as fixtures for the Wave 7 axe-core -Playwright spec (`tests/e2e/voyage-playground-a11y.spec.mjs`). - -Screenshots are NOT auto-committed. The `tests/e2e/snapshots/` directory -(produced by Playwright in Wave 7) is the test-baseline; this directory -holds curated illustrative captures for documentation. - -## Mappestruktur - -``` -docs/screenshots/ - README.md ← this file - dashboard/ ← project-dashboard fleet-grid views - artifact-detail/ ← drill-down render of brief/plan/review - annotation/ ← gutter-badge + sidebar + active highlight states - dark-mode/ ← screenshots taken with html[data-theme="dark"] - light-mode/ ← screenshots taken with html[data-theme="light"] -``` - -Each subfolder contains PNGs named by feature + variant, e.g. -`dashboard/fleet-grid-with-progress-tile.png`, -`annotation/active-anchor-yellow-tint.png`. - -## Hooks (window.__voyage) - -The playground exposes three automation hooks for headless screenshot -scripts. They are namespaced under `window.__voyage` to avoid global -pollution: - -| Method | Purpose | -|--------|---------| -| `window.__voyage.navigate(surface)` | Switch surface. `surface ∈ ['dashboard', 'detail', 'render', 'a11y']` | -| `window.__voyage.scheduleRender(state)` | Pre-populate state. `state.markdown` triggers `mountRender`; `state.artifacts` triggers `renderDashboard` | -| `window.__voyage.getProjectArtifacts()` | Returns the last-loaded `ProjectArtifacts` object (or `null`) | - -The hook surface mirrors the pattern used in -`plugins/llm-security/playground/llm-security-playground.html` -(`window.__navigate`, `window.__scheduleRender`). - -## Producing screenshots — Playwright headless (Wave 7+) - -Wave 7 ships a Playwright spec -(`tests/e2e/voyage-playground-a11y.spec.mjs`). Reuse its setup for -screenshots: - -```js -// tests/e2e/snapshot-helper.mjs (NOT committed in v4.3 — illustrative) -import { test } from '@playwright/test'; - -test('dashboard with all artifacts', async ({ page }) => { - await page.goto('file://' + process.cwd() + '/playground/voyage-playground.html'); - await page.evaluate(() => { - window.__voyage.scheduleRender({ - artifacts: { - basePath: 'demo', - brief: { path: 'brief.md', content: '...' }, - plan: { path: 'plan.md', content: '...' }, - review: null, - research: [], - architecture: { overview: null, gaps: null, looseFiles: [] }, - progress: null, - looseFiles: [], - } - }); - }); - await page.screenshot({ path: 'docs/screenshots/dashboard/fleet-grid-demo.png' }); -}); -``` - -## Producing screenshots — manuelt - -1. Åpne `playground/voyage-playground.html` i Chrome/Firefox. -2. Hvis du trenger et bestemt UI-state, paste relevant artifact-innhold i - textarea og trykk Render. -3. macOS: `Cmd+Shift+4`, deretter `Space` for vindu-skjermbilde. -4. Lagre med beskrivende filnavn under riktig undermappe ovenfor. - -## Konvensjon for nye screenshots - -- Bruk PNG, ikke JPG (lossless for tekst-tunge UIer). -- Kjør i konsistent viewport (1280×800 anbefales for desktop-baseline). -- Ta dark- og light-mode-varianter parvis når komponenten har visuelle - forskjeller mellom temaene. -- Ikke commit screenshots med personidentifiserende info eller hemmelig - artifact-innhold. diff --git a/plugins/voyage/lib/parsers/anchor-parser.mjs b/plugins/voyage/lib/parsers/anchor-parser.mjs deleted file mode 100644 index 634f7ce..0000000 --- a/plugins/voyage/lib/parsers/anchor-parser.mjs +++ /dev/null @@ -1,241 +0,0 @@ -// lib/parsers/anchor-parser.mjs -// Pure I/O-free parser for v4.2 voyage:anchor markdown comments. -// -// Anchor format (block-level only, on its own line, blank line above and below): -// -// -// Placement rules (validated by validateAnchorPlacement): -// - Not in list-items (Prettier #18066 progressive-whitespace bug) -// - Not inside fenced code blocks (`​``yaml`/`​``json`/etc.) -// - Not at line-start positions matching: --- frontmatter delimiter, -// manifest:, plan_version:, ### Step N:, ## , -// 40-char hex SHA1 (review finding-IDs) -// - ID must match /^ANN-\d{4}$/ -// - No duplicate IDs in same document -// -// Returns Result shape from lib/util/result.mjs. - -import { issue, ok, fail } from '../util/result.mjs'; - -const ANCHOR_LINE_RE = /^(\s*)\s*$/; -const ATTR_RE = /(\w+)="([^"]*)"/g; -const FENCED_OPEN_RE = /^```([a-zA-Z0-9_-]*)\s*$/; -const FENCED_CLOSE_RE = /^```\s*$/; -const LIST_ITEM_RE = /^\s*(?:[-*+]|\d+[.)])\s+/; -const ID_RE = /^ANN-\d{4}$/; -const FORBIDDEN_LINE_START = [ - /^---\s*$/, - /^manifest:\s*$/, - /^plan_version:/, - /^brief_version:/, - /^review_version:/, - /^### Step \d+:/, - /^## (?:Intent|Goal|Success Criteria|Executive Summary|Coverage|Remediation Summary)\b/, - /^[0-9a-f]{40}$/, -]; - -/** - * Parse anchor attributes string (the contents between voyage:anchor and -->). - * @returns {object} attribute map - */ -function parseAttrs(s) { - const attrs = {}; - let m; - ATTR_RE.lastIndex = 0; - while ((m = ATTR_RE.exec(s)) !== null) { - attrs[m[1]] = m[2]; - } - return attrs; -} - -/** - * Parse all anchor comments in a markdown document. - * @param {string} md - * @returns {Result} { valid, errors, warnings, parsed: Anchor[] } - */ -export function parseAnchors(md) { - if (typeof md !== 'string') { - return fail(issue('ANCHOR_INPUT', 'Input must be a string')); - } - const lines = md.split(/\r?\n/); - const anchors = []; - const errors = []; - const warnings = []; - const seenIds = new Set(); - let inFence = false; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (FENCED_OPEN_RE.test(line)) { - inFence = !inFence; - continue; - } - if (inFence && FENCED_CLOSE_RE.test(line)) { - inFence = false; - continue; - } - if (inFence) continue; - - const m = line.match(ANCHOR_LINE_RE); - if (!m) continue; - const attrs = parseAttrs(m[2]); - - if (!attrs.id) { - errors.push(issue('ANCHOR_MALFORMED', `Anchor at line ${i + 1} missing required id attribute`)); - continue; - } - if (!ID_RE.test(attrs.id)) { - errors.push(issue('ANCHOR_BAD_ID', `Anchor id "${attrs.id}" at line ${i + 1} does not match /^ANN-\\d{4}$/`)); - continue; - } - if (seenIds.has(attrs.id)) { - errors.push(issue('ANCHOR_DUPLICATE_ID', `Duplicate anchor id "${attrs.id}" at line ${i + 1}`)); - continue; - } - seenIds.add(attrs.id); - - if (!attrs.target) { - errors.push(issue('ANCHOR_MALFORMED', `Anchor "${attrs.id}" at line ${i + 1} missing required target attribute`)); - continue; - } - - if (attrs.snippet && attrs.snippet.length > 80) { - warnings.push(issue('ANCHOR_SNIPPET_TRUNCATED', `Anchor "${attrs.id}" snippet > 80 chars (${attrs.snippet.length})`)); - } - - if (attrs.intent && !['fix', 'change', 'question', 'block'].includes(attrs.intent)) { - warnings.push(issue('ANCHOR_BAD_INTENT', `Anchor "${attrs.id}" intent "${attrs.intent}" not in {fix|change|question|block}`)); - } - - anchors.push({ - id: attrs.id, - target: attrs.target, - line: attrs.line ? Number.parseInt(attrs.line, 10) : null, - snippet: attrs.snippet || null, - intent: attrs.intent || null, - raw: line, - position: { line: i + 1, col: 0 }, - }); - } - - if (errors.length > 0) return { valid: false, errors, warnings, parsed: anchors }; - return { valid: true, errors: [], warnings, parsed: anchors }; -} - -/** - * Insert anchor comments into markdown above target lines. - * Each anchor inserted on its own line with blank line separation. - * - * @param {string} md - source markdown - * @param {Array<{id, target, line, snippet?, intent?}>} anchors - anchors to insert (sorted by line ASC) - * @returns {string} markdown with anchors injected - */ -export function addAnchors(md, anchors) { - if (typeof md !== 'string') return md; - if (!Array.isArray(anchors) || anchors.length === 0) return md; - - const lines = md.split(/\r?\n/); - // Sort by line desc so insertions don't shift later line numbers - const sorted = [...anchors].sort((a, b) => (b.line || 0) - (a.line || 0)); - - for (const a of sorted) { - if (!a.line || a.line < 1 || a.line > lines.length + 1) continue; - const attrs = [`id="${a.id}"`, `target="${a.target}"`, `line="${a.line}"`]; - if (a.snippet) attrs.push(`snippet="${a.snippet.slice(0, 80)}"`); - if (a.intent) attrs.push(`intent="${a.intent}"`); - const anchorLine = ``; - // Insert above target line: anchorLine + blank line, then target stays - lines.splice(a.line - 1, 0, anchorLine, ''); - } - return lines.join('\n'); -} - -/** - * Strip all voyage:anchor comments from markdown, restoring the original. - * Matches the format produced by addAnchors() — anchor line + following blank. - * - * @param {string} md - * @returns {string} markdown with anchors removed - */ -export function stripAnchors(md) { - if (typeof md !== 'string') return md; - const lines = md.split(/\r?\n/); - const out = []; - for (let i = 0; i < lines.length; i++) { - if (ANCHOR_LINE_RE.test(lines[i])) { - // Skip anchor line; if next line is blank (separator inserted by addAnchors), skip it too - if (i + 1 < lines.length && lines[i + 1].trim() === '') i++; - continue; - } - out.push(lines[i]); - } - return out.join('\n'); -} - -/** - * Validate anchor placement against voyage's structural constraints. - * Returns errors for placement violations; does not mutate input. - * - * @param {string} md - * @param {Anchor[]} anchors - * @returns {Result} - */ -export function validateAnchorPlacement(md, anchors) { - if (typeof md !== 'string') { - return fail(issue('ANCHOR_INPUT', 'Input must be a string')); - } - const lines = md.split(/\r?\n/); - const errors = []; - - // Build fenced-block ranges - const fenced = []; // [{startLine, endLine}] - let openLine = null; - for (let i = 0; i < lines.length; i++) { - if (FENCED_OPEN_RE.test(lines[i])) { - if (openLine === null) { - openLine = i; - } else { - fenced.push({ startLine: openLine, endLine: i }); - openLine = null; - } - } - } - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (!ANCHOR_LINE_RE.test(line)) continue; - - // Inside fenced block? - for (const f of fenced) { - if (i > f.startLine && i < f.endLine) { - errors.push(issue('ANCHOR_IN_FENCED_BLOCK', `Anchor at line ${i + 1} is inside fenced code block (lines ${f.startLine + 1}-${f.endLine + 1}); move it above or below the fence`)); - break; - } - } - - // List item context: either the anchor line itself starts with a list-marker, - // OR the anchor line is indented (whitespace-prefixed) AND the previous - // non-empty line is a list item. v4.2 disipline: anchors must start at col 0. - if (LIST_ITEM_RE.test(line)) { - errors.push(issue('ANCHOR_IN_LIST_ITEM', `Anchor at line ${i + 1} is inside a list-item (Prettier #18066 issue — move above the list)`)); - } else if (/^\s+= 0 && lines[j].trim() === '') j--; - if (j >= 0 && (LIST_ITEM_RE.test(lines[j]) || /^\s+(?:[-*+]|\d+[.)])\s/.test(lines[j]))) { - errors.push(issue('ANCHOR_IN_LIST_ITEM', `Anchor at line ${i + 1} is indented after a list-item — move to col 0 above the list`)); - } - } - - // Forbidden line-start collision check: the anchor itself starts with `` -kommentarer i rå-markdown blir til klikkbare gutters med yellow-tint highlight -i rendered markdown. Block-boundary-fallback håndterer code-fences, tables, og -list-items per Prettier #18066-workaround. - -## Bruk - -### Webkitdirectory-velger (anbefalt) - -Klikk **"Velg prosjekt-mappe"**-knappen og velg din `.claude/projects//` -i nettleserens directory-picker. Krever Chromium-baserte nettlesere -(Chrome / Edge / Brave / Arc) — Firefox støtter `webkitdirectory` siden v150 -men har en kjent Windows-bug (se Begrensninger). - -### Drag-drop - -Dra hele prosjekt-mappen direkte fra Finder/Explorer over playground-vinduet. -Drop-zone aktiveres ved `dragenter`. Bruker `webkitGetAsEntry()` for rekursiv -mappe-walk. - -### URL-parameter `?project=` - -Som ergonomisk shortcut kan du seede URL-en med en absolutt sti: - -``` -file:///path/to/playground/voyage-playground.html?project=/abs/path/to/project -``` - -URL-parameteren skriver til localStorage som default-prosjekt. Trenger fortsatt -en webkitdirectory-velger eller drag-drop første gang for å gi nettleseren -fil-tilgang (file:// + JS sandbox kan ikke åpne arbitrære lokale stier uten -brukergeste). - -## Discoverability av `.claude/`-mappen - -`.claude/projects/` er hidden by default på macOS / Linux. For å åpne den i -Finder: - -```bash -# macOS -open .claude/projects/ - -# Linux -xdg-open .claude/projects/ - -# Windows (PowerShell) -explorer .claude/projects/ -``` - -I directory-picker kan du også taste `.` for å vise hidden folders. - -## Annotation-flow - -1. **Rediger:** klikk på en gutter-badge eller bruk `J`/`K` for å navigere mellom - anchors. `Esc` lukker sidebar. -2. **Lagre:** annotations persisteres til `localStorage` med key - `voyage_ann_`. -3. **Eksporter:** klikk **"Eksporter annotert markdown"** for å laste ned - `annotated-{target}.md` (target = brief|plan|review|artifact basert på - frontmatter). Filen kan deretter mates til `/trekrevise --project ` - for å folde annotasjonene tilbake i source-artifaktet. - -## Hooks for screenshot-automatisering - -`window.__voyage` (Wave 5 Step 23) eksponerer tre metoder for Playwright + -manuell screenshot-prosedyre: - -- `window.__voyage.navigate(target)` — prog-navigerer til artifact-detail -- `window.__voyage.scheduleRender({ a11yViolations })` — populerer A11Y-panel -- `window.__voyage.getProjectArtifacts()` — returnerer aktiv ProjectArtifacts - -Se `docs/screenshots/README.md` for fullstendig manuell og automatisert -screenshot-prosedyre. - -## Begrensninger - -- **Firefox 150 Windows drag-drop bug:** UA-detect-guard viser fallback-melding - som ber bruker bruke `webkitdirectory`-velgeren istedenfor. -- **Ingen File System Access (FSA):** voyage skriver ikke direkte til disk. - Annotations lagres i `localStorage`; eksport går via `Blob` + nedlasting. -- **`design/`-mappen out-of-scope:** voyage utforsker ikke `/design/`-mapper - i v4.3. -- **Kjente WCAG-violations:** v4.3 har baseline'd WCAG-violations - (`aria-hidden-focus`, `color-contrast` på `.key-stat--critical__label`) - defer til v4.4. Se `tests/e2e/snapshots/a11y-baseline.json`. - -## Bundle-størrelse - -Total vendored bundle: ~388 KB (under 460 KB HALT-gate). Inkluderer: - -- `markdown-it` ~115 KB -- `gray-matter` (front-matter) ~36 KB -- `highlight.js` minimal subset ~120 KB -- `DOMPurify` 3.2.6 ~22 KB (Wave 5 Step 24) -- `playground-design-system` ~95 KB - -Alle vendored under `playground/lib/` + `playground/vendor/playground-design-system/` — -ingen CDN-kall (SC7 verifisert via Playwright network-intercept i -`tests/e2e/voyage-playground-network.spec.mjs`). - -## Tester - -- `tests/playground/voyage-playground.test.mjs` — Group A static-HTML - assertions (SC1 10-element + SC3 + SC6 + SC7) -- `tests/playground/voyage-playground-structure.test.mjs` — Group B DS-token - + theme-toggle + sidebar-tab + keyboard-pattern -- `tests/integration/annotation-export-schema.test.mjs` — Group C export-bundle - schema + annotation_digest validity -- `tests/e2e/voyage-playground-a11y.spec.mjs` — Group D Playwright a11y - delta-baseline + pixel-diff -- `tests/e2e/voyage-playground-network.spec.mjs` — Group D SC7 zero-external- - requests authoritative gate - -`npm test` kjører Groups A/B/C (Node-test, raskt). `npm run test:e2e` kjører -Group D (Playwright + Chromium). diff --git a/plugins/voyage/playground/lib/VENDOR-MANIFEST.json b/plugins/voyage/playground/lib/VENDOR-MANIFEST.json deleted file mode 100644 index 9e73cb6..0000000 --- a/plugins/voyage/playground/lib/VENDOR-MANIFEST.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "generated_at": "2026-05-10T15:59:53.379Z", - "pins": { - "markdown-it": "14.1.0", - "markdown-it-front-matter": "0.2.4", - "highlight.js": "11.11.1", - "dompurify": "3.2.6" - }, - "highlight_languages": [ - "yaml", - "json", - "javascript", - "bash", - "markdown", - "diff" - ], - "output_files": [ - "markdown-it.min.js", - "markdown-it-front-matter.min.js", - "highlight.min.js", - "dompurify.min.js" - ] -} diff --git a/plugins/voyage/playground/lib/dompurify.min.js b/plugins/voyage/playground/lib/dompurify.min.js deleted file mode 100644 index 73df78d..0000000 --- a/plugins/voyage/playground/lib/dompurify.min.js +++ /dev/null @@ -1,3 +0,0 @@ -/*! @license DOMPurify 3.2.6 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.2.6/LICENSE */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=R(Array.prototype.forEach),m=R(Array.prototype.lastIndexOf),p=R(Array.prototype.pop),f=R(Array.prototype.push),d=R(Array.prototype.splice),h=R(String.prototype.toLowerCase),g=R(String.prototype.toString),T=R(String.prototype.match),y=R(String.prototype.replace),E=R(String.prototype.indexOf),A=R(String.prototype.trim),_=R(Object.prototype.hasOwnProperty),S=R(RegExp.prototype.test),b=(N=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n1?n-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:h;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function O(e){for(let t=0;t/gm),G=a(/\$\{[\w\W]*/gm),Y=a(/^data-[\-\w.\u00B7-\uFFFF]+$/),j=a(/^aria-[\-\w]+$/),X=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),q=a(/^(?:\w+script|data):/i),$=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),K=a(/^html$/i),V=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var Z=Object.freeze({__proto__:null,ARIA_ATTR:j,ATTR_WHITESPACE:$,CUSTOM_ELEMENT:V,DATA_ATTR:Y,DOCTYPE_NAME:K,ERB_EXPR:W,IS_ALLOWED_URI:X,IS_SCRIPT_OR_DATA:q,MUSTACHE_EXPR:B,TMPLIT_EXPR:G});const J=1,Q=3,ee=7,te=8,ne=9,oe=function(){return"undefined"==typeof window?null:window};var re=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:oe();const o=e=>t(e);if(o.version="3.2.6",o.removed=[],!n||!n.document||n.document.nodeType!==ne||!n.Element)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:N,Node:R,Element:O,NodeFilter:B,NamedNodeMap:W=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:G,DOMParser:Y,trustedTypes:j}=n,q=O.prototype,$=v(q,"cloneNode"),V=v(q,"remove"),re=v(q,"nextSibling"),ie=v(q,"childNodes"),ae=v(q,"parentNode");if("function"==typeof N){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let le,ce="";const{implementation:se,createNodeIterator:ue,createDocumentFragment:me,getElementsByTagName:pe}=r,{importNode:fe}=a;let de={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]};o.isSupported="function"==typeof e&&"function"==typeof ae&&se&&void 0!==se.createHTMLDocument;const{MUSTACHE_EXPR:he,ERB_EXPR:ge,TMPLIT_EXPR:Te,DATA_ATTR:ye,ARIA_ATTR:Ee,IS_SCRIPT_OR_DATA:Ae,ATTR_WHITESPACE:_e,CUSTOM_ELEMENT:Se}=Z;let{IS_ALLOWED_URI:be}=Z,Ne=null;const Re=w({},[...L,...C,...x,...M,...U]);let we=null;const Oe=w({},[...z,...P,...H,...F]);let De=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),ve=null,Le=null,Ce=!0,xe=!0,Ie=!1,Me=!0,ke=!1,Ue=!0,ze=!1,Pe=!1,He=!1,Fe=!1,Be=!1,We=!1,Ge=!0,Ye=!1,je=!0,Xe=!1,qe={},$e=null;const Ke=w({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ve=null;const Ze=w({},["audio","video","img","source","image","track"]);let Je=null;const Qe=w({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),et="http://www.w3.org/1998/Math/MathML",tt="http://www.w3.org/2000/svg",nt="http://www.w3.org/1999/xhtml";let ot=nt,rt=!1,it=null;const at=w({},[et,tt,nt],g);let lt=w({},["mi","mo","mn","ms","mtext"]),ct=w({},["annotation-xml"]);const st=w({},["title","style","font","a","script"]);let ut=null;const mt=["application/xhtml+xml","text/html"];let pt=null,ft=null;const dt=r.createElement("form"),ht=function(e){return e instanceof RegExp||e instanceof Function},gt=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!ft||ft!==e){if(e&&"object"==typeof e||(e={}),e=D(e),ut=-1===mt.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,pt="application/xhtml+xml"===ut?g:h,Ne=_(e,"ALLOWED_TAGS")?w({},e.ALLOWED_TAGS,pt):Re,we=_(e,"ALLOWED_ATTR")?w({},e.ALLOWED_ATTR,pt):Oe,it=_(e,"ALLOWED_NAMESPACES")?w({},e.ALLOWED_NAMESPACES,g):at,Je=_(e,"ADD_URI_SAFE_ATTR")?w(D(Qe),e.ADD_URI_SAFE_ATTR,pt):Qe,Ve=_(e,"ADD_DATA_URI_TAGS")?w(D(Ze),e.ADD_DATA_URI_TAGS,pt):Ze,$e=_(e,"FORBID_CONTENTS")?w({},e.FORBID_CONTENTS,pt):Ke,ve=_(e,"FORBID_TAGS")?w({},e.FORBID_TAGS,pt):D({}),Le=_(e,"FORBID_ATTR")?w({},e.FORBID_ATTR,pt):D({}),qe=!!_(e,"USE_PROFILES")&&e.USE_PROFILES,Ce=!1!==e.ALLOW_ARIA_ATTR,xe=!1!==e.ALLOW_DATA_ATTR,Ie=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Me=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,ke=e.SAFE_FOR_TEMPLATES||!1,Ue=!1!==e.SAFE_FOR_XML,ze=e.WHOLE_DOCUMENT||!1,Fe=e.RETURN_DOM||!1,Be=e.RETURN_DOM_FRAGMENT||!1,We=e.RETURN_TRUSTED_TYPE||!1,He=e.FORCE_BODY||!1,Ge=!1!==e.SANITIZE_DOM,Ye=e.SANITIZE_NAMED_PROPS||!1,je=!1!==e.KEEP_CONTENT,Xe=e.IN_PLACE||!1,be=e.ALLOWED_URI_REGEXP||X,ot=e.NAMESPACE||nt,lt=e.MATHML_TEXT_INTEGRATION_POINTS||lt,ct=e.HTML_INTEGRATION_POINTS||ct,De=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&ht(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(De.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&ht(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(De.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(De.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),ke&&(xe=!1),Be&&(Fe=!0),qe&&(Ne=w({},U),we=[],!0===qe.html&&(w(Ne,L),w(we,z)),!0===qe.svg&&(w(Ne,C),w(we,P),w(we,F)),!0===qe.svgFilters&&(w(Ne,x),w(we,P),w(we,F)),!0===qe.mathMl&&(w(Ne,M),w(we,H),w(we,F))),e.ADD_TAGS&&(Ne===Re&&(Ne=D(Ne)),w(Ne,e.ADD_TAGS,pt)),e.ADD_ATTR&&(we===Oe&&(we=D(we)),w(we,e.ADD_ATTR,pt)),e.ADD_URI_SAFE_ATTR&&w(Je,e.ADD_URI_SAFE_ATTR,pt),e.FORBID_CONTENTS&&($e===Ke&&($e=D($e)),w($e,e.FORBID_CONTENTS,pt)),je&&(Ne["#text"]=!0),ze&&w(Ne,["html","head","body"]),Ne.table&&(w(Ne,["tbody"]),delete ve.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw b('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw b('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');le=e.TRUSTED_TYPES_POLICY,ce=le.createHTML("")}else void 0===le&&(le=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}}(j,c)),null!==le&&"string"==typeof ce&&(ce=le.createHTML(""));i&&i(e),ft=e}},Tt=w({},[...C,...x,...I]),yt=w({},[...M,...k]),Et=function(e){f(o.removed,{element:e});try{ae(e).removeChild(e)}catch(t){V(e)}},At=function(e,t){try{f(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){f(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e)if(Fe||Be)try{Et(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},_t=function(e){let t=null,n=null;if(He)e=""+e;else{const t=T(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===ut&&ot===nt&&(e=''+e+"");const o=le?le.createHTML(e):e;if(ot===nt)try{t=(new Y).parseFromString(o,ut)}catch(e){}if(!t||!t.documentElement){t=se.createDocument(ot,"template",null);try{t.documentElement.innerHTML=rt?ce:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),ot===nt?pe.call(t,ze?"html":"body")[0]:ze?t.documentElement:i},St=function(e){return ue.call(e.ownerDocument||e,e,B.SHOW_ELEMENT|B.SHOW_COMMENT|B.SHOW_TEXT|B.SHOW_PROCESSING_INSTRUCTION|B.SHOW_CDATA_SECTION,null)},bt=function(e){return e instanceof G&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof W)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},Nt=function(e){return"function"==typeof R&&e instanceof R};function Rt(e,t,n){u(e,(e=>{e.call(o,t,n,ft)}))}const wt=function(e){let t=null;if(Rt(de.beforeSanitizeElements,e,null),bt(e))return Et(e),!0;const n=pt(e.nodeName);if(Rt(de.uponSanitizeElement,e,{tagName:n,allowedTags:Ne}),Ue&&e.hasChildNodes()&&!Nt(e.firstElementChild)&&S(/<[/\w!]/g,e.innerHTML)&&S(/<[/\w!]/g,e.textContent))return Et(e),!0;if(e.nodeType===ee)return Et(e),!0;if(Ue&&e.nodeType===te&&S(/<[/\w]/g,e.data))return Et(e),!0;if(!Ne[n]||ve[n]){if(!ve[n]&&Dt(n)){if(De.tagNameCheck instanceof RegExp&&S(De.tagNameCheck,n))return!1;if(De.tagNameCheck instanceof Function&&De.tagNameCheck(n))return!1}if(je&&!$e[n]){const t=ae(e)||e.parentNode,n=ie(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o){const r=$(n[o],!0);r.__removalCount=(e.__removalCount||0)+1,t.insertBefore(r,re(e))}}}return Et(e),!0}return e instanceof O&&!function(e){let t=ae(e);t&&t.tagName||(t={namespaceURI:ot,tagName:"template"});const n=h(e.tagName),o=h(t.tagName);return!!it[e.namespaceURI]&&(e.namespaceURI===tt?t.namespaceURI===nt?"svg"===n:t.namespaceURI===et?"svg"===n&&("annotation-xml"===o||lt[o]):Boolean(Tt[n]):e.namespaceURI===et?t.namespaceURI===nt?"math"===n:t.namespaceURI===tt?"math"===n&&ct[o]:Boolean(yt[n]):e.namespaceURI===nt?!(t.namespaceURI===tt&&!ct[o])&&!(t.namespaceURI===et&&!lt[o])&&!yt[n]&&(st[n]||!Tt[n]):!("application/xhtml+xml"!==ut||!it[e.namespaceURI]))}(e)?(Et(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!S(/<\/no(script|embed|frames)/i,e.innerHTML)?(ke&&e.nodeType===Q&&(t=e.textContent,u([he,ge,Te],(e=>{t=y(t,e," ")})),e.textContent!==t&&(f(o.removed,{element:e.cloneNode()}),e.textContent=t)),Rt(de.afterSanitizeElements,e,null),!1):(Et(e),!0)},Ot=function(e,t,n){if(Ge&&("id"===t||"name"===t)&&(n in r||n in dt))return!1;if(xe&&!Le[t]&&S(ye,t));else if(Ce&&S(Ee,t));else if(!we[t]||Le[t]){if(!(Dt(e)&&(De.tagNameCheck instanceof RegExp&&S(De.tagNameCheck,e)||De.tagNameCheck instanceof Function&&De.tagNameCheck(e))&&(De.attributeNameCheck instanceof RegExp&&S(De.attributeNameCheck,t)||De.attributeNameCheck instanceof Function&&De.attributeNameCheck(t))||"is"===t&&De.allowCustomizedBuiltInElements&&(De.tagNameCheck instanceof RegExp&&S(De.tagNameCheck,n)||De.tagNameCheck instanceof Function&&De.tagNameCheck(n))))return!1}else if(Je[t]);else if(S(be,y(n,_e,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==E(n,"data:")||!Ve[e]){if(Ie&&!S(Ae,y(n,_e,"")));else if(n)return!1}else;return!0},Dt=function(e){return"annotation-xml"!==e&&T(e,Se)},vt=function(e){Rt(de.beforeSanitizeAttributes,e,null);const{attributes:t}=e;if(!t||bt(e))return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:we,forceKeepAttr:void 0};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=pt(a),m=c;let f="value"===a?m:A(m);if(n.attrName=s,n.attrValue=f,n.keepAttr=!0,n.forceKeepAttr=void 0,Rt(de.uponSanitizeAttribute,e,n),f=n.attrValue,!Ye||"id"!==s&&"name"!==s||(At(a,e),f="user-content-"+f),Ue&&S(/((--!?|])>)|<\/(style|title)/i,f)){At(a,e);continue}if(n.forceKeepAttr)continue;if(!n.keepAttr){At(a,e);continue}if(!Me&&S(/\/>/i,f)){At(a,e);continue}ke&&u([he,ge,Te],(e=>{f=y(f,e," ")}));const d=pt(e.nodeName);if(Ot(d,s,f)){if(le&&"object"==typeof j&&"function"==typeof j.getAttributeType)if(l);else switch(j.getAttributeType(d,s)){case"TrustedHTML":f=le.createHTML(f);break;case"TrustedScriptURL":f=le.createScriptURL(f)}if(f!==m)try{l?e.setAttributeNS(l,a,f):e.setAttribute(a,f),bt(e)?Et(e):p(o.removed)}catch(t){At(a,e)}}else At(a,e)}Rt(de.afterSanitizeAttributes,e,null)},Lt=function e(t){let n=null;const o=St(t);for(Rt(de.beforeSanitizeShadowDOM,t,null);n=o.nextNode();)Rt(de.uponSanitizeShadowNode,n,null),wt(n),vt(n),n.content instanceof s&&e(n.content);Rt(de.afterSanitizeShadowDOM,t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(rt=!e,rt&&(e="\x3c!--\x3e"),"string"!=typeof e&&!Nt(e)){if("function"!=typeof e.toString)throw b("toString is not a function");if("string"!=typeof(e=e.toString()))throw b("dirty is not a string, aborting")}if(!o.isSupported)return e;if(Pe||gt(t),o.removed=[],"string"==typeof e&&(Xe=!1),Xe){if(e.nodeName){const t=pt(e.nodeName);if(!Ne[t]||ve[t])throw b("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof R)n=_t("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),r.nodeType===J&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!Fe&&!ke&&!ze&&-1===e.indexOf("<"))return le&&We?le.createHTML(e):e;if(n=_t(e),!n)return Fe?null:We?ce:""}n&&He&&Et(n.firstChild);const c=St(Xe?e:n);for(;i=c.nextNode();)wt(i),vt(i),i.content instanceof s&&Lt(i.content);if(Xe)return e;if(Fe){if(Be)for(l=me.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(we.shadowroot||we.shadowrootmode)&&(l=fe.call(a,l,!0)),l}let m=ze?n.outerHTML:n.innerHTML;return ze&&Ne["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&S(K,n.ownerDocument.doctype.name)&&(m="\n"+m),ke&&u([he,ge,Te],(e=>{m=y(m,e," ")})),le&&We?le.createHTML(m):m},o.setConfig=function(){gt(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),Pe=!0},o.clearConfig=function(){ft=null,Pe=!1},o.isValidAttribute=function(e,t,n){ft||gt({});const o=pt(e),r=pt(t);return Ot(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&f(de[e],t)},o.removeHook=function(e,t){if(void 0!==t){const n=m(de[e],t);return-1===n?void 0:d(de[e],n,1)[0]}return p(de[e])},o.removeHooks=function(e){de[e]=[]},o.removeAllHooks=function(){de={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},o}();return re})); -//# sourceMappingURL=purify.min.js.map diff --git a/plugins/voyage/playground/lib/highlight.min.js b/plugins/voyage/playground/lib/highlight.min.js deleted file mode 100644 index 14b4263..0000000 --- a/plugins/voyage/playground/lib/highlight.min.js +++ /dev/null @@ -1,25 +0,0 @@ -// vendored by scripts/vendor-playground-libs.mjs — DO NOT EDIT -// global: hljs (highlight.js@11.11.1 — core + yaml/json/javascript/bash/markdown/diff) -(function (root) { - function loadCommonJS(src) { - var __mod = { exports: {} }; - var fn = new Function('module', 'exports', src); - fn(__mod, __mod.exports); - return __mod.exports; - } - var coreSrc = "/* eslint-disable no-multi-assign */\n\nfunction deepFreeze(obj) {\n if (obj instanceof Map) {\n obj.clear =\n obj.delete =\n obj.set =\n function () {\n throw new Error('map is read-only');\n };\n } else if (obj instanceof Set) {\n obj.add =\n obj.clear =\n obj.delete =\n function () {\n throw new Error('set is read-only');\n };\n }\n\n // Freeze self\n Object.freeze(obj);\n\n Object.getOwnPropertyNames(obj).forEach((name) => {\n const prop = obj[name];\n const type = typeof prop;\n\n // Freeze prop if it is an object or function and also not already frozen\n if ((type === 'object' || type === 'function') && !Object.isFrozen(prop)) {\n deepFreeze(prop);\n }\n });\n\n return obj;\n}\n\n/** @typedef {import('highlight.js').CallbackResponse} CallbackResponse */\n/** @typedef {import('highlight.js').CompiledMode} CompiledMode */\n/** @implements CallbackResponse */\n\nclass Response {\n /**\n * @param {CompiledMode} mode\n */\n constructor(mode) {\n // eslint-disable-next-line no-undefined\n if (mode.data === undefined) mode.data = {};\n\n this.data = mode.data;\n this.isMatchIgnored = false;\n }\n\n ignoreMatch() {\n this.isMatchIgnored = true;\n }\n}\n\n/**\n * @param {string} value\n * @returns {string}\n */\nfunction escapeHTML(value) {\n return value\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * performs a shallow merge of multiple objects into one\n *\n * @template T\n * @param {T} original\n * @param {Record[]} objects\n * @returns {T} a single new object\n */\nfunction inherit$1(original, ...objects) {\n /** @type Record */\n const result = Object.create(null);\n\n for (const key in original) {\n result[key] = original[key];\n }\n objects.forEach(function(obj) {\n for (const key in obj) {\n result[key] = obj[key];\n }\n });\n return /** @type {T} */ (result);\n}\n\n/**\n * @typedef {object} Renderer\n * @property {(text: string) => void} addText\n * @property {(node: Node) => void} openNode\n * @property {(node: Node) => void} closeNode\n * @property {() => string} value\n */\n\n/** @typedef {{scope?: string, language?: string, sublanguage?: boolean}} Node */\n/** @typedef {{walk: (r: Renderer) => void}} Tree */\n/** */\n\nconst SPAN_CLOSE = '';\n\n/**\n * Determines if a node needs to be wrapped in \n *\n * @param {Node} node */\nconst emitsWrappingTags = (node) => {\n // rarely we can have a sublanguage where language is undefined\n // TODO: track down why\n return !!node.scope;\n};\n\n/**\n *\n * @param {string} name\n * @param {{prefix:string}} options\n */\nconst scopeToCSSClass = (name, { prefix }) => {\n // sub-language\n if (name.startsWith(\"language:\")) {\n return name.replace(\"language:\", \"language-\");\n }\n // tiered scope: comment.line\n if (name.includes(\".\")) {\n const pieces = name.split(\".\");\n return [\n `${prefix}${pieces.shift()}`,\n ...(pieces.map((x, i) => `${x}${\"_\".repeat(i + 1)}`))\n ].join(\" \");\n }\n // simple scope\n return `${prefix}${name}`;\n};\n\n/** @type {Renderer} */\nclass HTMLRenderer {\n /**\n * Creates a new HTMLRenderer\n *\n * @param {Tree} parseTree - the parse tree (must support `walk` API)\n * @param {{classPrefix: string}} options\n */\n constructor(parseTree, options) {\n this.buffer = \"\";\n this.classPrefix = options.classPrefix;\n parseTree.walk(this);\n }\n\n /**\n * Adds texts to the output stream\n *\n * @param {string} text */\n addText(text) {\n this.buffer += escapeHTML(text);\n }\n\n /**\n * Adds a node open to the output stream (if needed)\n *\n * @param {Node} node */\n openNode(node) {\n if (!emitsWrappingTags(node)) return;\n\n const className = scopeToCSSClass(node.scope,\n { prefix: this.classPrefix });\n this.span(className);\n }\n\n /**\n * Adds a node close to the output stream (if needed)\n *\n * @param {Node} node */\n closeNode(node) {\n if (!emitsWrappingTags(node)) return;\n\n this.buffer += SPAN_CLOSE;\n }\n\n /**\n * returns the accumulated buffer\n */\n value() {\n return this.buffer;\n }\n\n // helpers\n\n /**\n * Builds a span element\n *\n * @param {string} className */\n span(className) {\n this.buffer += ``;\n }\n}\n\n/** @typedef {{scope?: string, language?: string, children: Node[]} | string} Node */\n/** @typedef {{scope?: string, language?: string, children: Node[]} } DataNode */\n/** @typedef {import('highlight.js').Emitter} Emitter */\n/** */\n\n/** @returns {DataNode} */\nconst newNode = (opts = {}) => {\n /** @type DataNode */\n const result = { children: [] };\n Object.assign(result, opts);\n return result;\n};\n\nclass TokenTree {\n constructor() {\n /** @type DataNode */\n this.rootNode = newNode();\n this.stack = [this.rootNode];\n }\n\n get top() {\n return this.stack[this.stack.length - 1];\n }\n\n get root() { return this.rootNode; }\n\n /** @param {Node} node */\n add(node) {\n this.top.children.push(node);\n }\n\n /** @param {string} scope */\n openNode(scope) {\n /** @type Node */\n const node = newNode({ scope });\n this.add(node);\n this.stack.push(node);\n }\n\n closeNode() {\n if (this.stack.length > 1) {\n return this.stack.pop();\n }\n // eslint-disable-next-line no-undefined\n return undefined;\n }\n\n closeAllNodes() {\n while (this.closeNode());\n }\n\n toJSON() {\n return JSON.stringify(this.rootNode, null, 4);\n }\n\n /**\n * @typedef { import(\"./html_renderer\").Renderer } Renderer\n * @param {Renderer} builder\n */\n walk(builder) {\n // this does not\n return this.constructor._walk(builder, this.rootNode);\n // this works\n // return TokenTree._walk(builder, this.rootNode);\n }\n\n /**\n * @param {Renderer} builder\n * @param {Node} node\n */\n static _walk(builder, node) {\n if (typeof node === \"string\") {\n builder.addText(node);\n } else if (node.children) {\n builder.openNode(node);\n node.children.forEach((child) => this._walk(builder, child));\n builder.closeNode(node);\n }\n return builder;\n }\n\n /**\n * @param {Node} node\n */\n static _collapse(node) {\n if (typeof node === \"string\") return;\n if (!node.children) return;\n\n if (node.children.every(el => typeof el === \"string\")) {\n // node.text = node.children.join(\"\");\n // delete node.children;\n node.children = [node.children.join(\"\")];\n } else {\n node.children.forEach((child) => {\n TokenTree._collapse(child);\n });\n }\n }\n}\n\n/**\n Currently this is all private API, but this is the minimal API necessary\n that an Emitter must implement to fully support the parser.\n\n Minimal interface:\n\n - addText(text)\n - __addSublanguage(emitter, subLanguageName)\n - startScope(scope)\n - endScope()\n - finalize()\n - toHTML()\n\n*/\n\n/**\n * @implements {Emitter}\n */\nclass TokenTreeEmitter extends TokenTree {\n /**\n * @param {*} options\n */\n constructor(options) {\n super();\n this.options = options;\n }\n\n /**\n * @param {string} text\n */\n addText(text) {\n if (text === \"\") { return; }\n\n this.add(text);\n }\n\n /** @param {string} scope */\n startScope(scope) {\n this.openNode(scope);\n }\n\n endScope() {\n this.closeNode();\n }\n\n /**\n * @param {Emitter & {root: DataNode}} emitter\n * @param {string} name\n */\n __addSublanguage(emitter, name) {\n /** @type DataNode */\n const node = emitter.root;\n if (name) node.scope = `language:${name}`;\n\n this.add(node);\n }\n\n toHTML() {\n const renderer = new HTMLRenderer(this, this.options);\n return renderer.value();\n }\n\n finalize() {\n this.closeAllNodes();\n return true;\n }\n}\n\n/**\n * @param {string} value\n * @returns {RegExp}\n * */\n\n/**\n * @param {RegExp | string } re\n * @returns {string}\n */\nfunction source(re) {\n if (!re) return null;\n if (typeof re === \"string\") return re;\n\n return re.source;\n}\n\n/**\n * @param {RegExp | string } re\n * @returns {string}\n */\nfunction lookahead(re) {\n return concat('(?=', re, ')');\n}\n\n/**\n * @param {RegExp | string } re\n * @returns {string}\n */\nfunction anyNumberOfTimes(re) {\n return concat('(?:', re, ')*');\n}\n\n/**\n * @param {RegExp | string } re\n * @returns {string}\n */\nfunction optional(re) {\n return concat('(?:', re, ')?');\n}\n\n/**\n * @param {...(RegExp | string) } args\n * @returns {string}\n */\nfunction concat(...args) {\n const joined = args.map((x) => source(x)).join(\"\");\n return joined;\n}\n\n/**\n * @param { Array } args\n * @returns {object}\n */\nfunction stripOptionsFromArgs(args) {\n const opts = args[args.length - 1];\n\n if (typeof opts === 'object' && opts.constructor === Object) {\n args.splice(args.length - 1, 1);\n return opts;\n } else {\n return {};\n }\n}\n\n/** @typedef { {capture?: boolean} } RegexEitherOptions */\n\n/**\n * Any of the passed expresssions may match\n *\n * Creates a huge this | this | that | that match\n * @param {(RegExp | string)[] | [...(RegExp | string)[], RegexEitherOptions]} args\n * @returns {string}\n */\nfunction either(...args) {\n /** @type { object & {capture?: boolean} } */\n const opts = stripOptionsFromArgs(args);\n const joined = '('\n + (opts.capture ? \"\" : \"?:\")\n + args.map((x) => source(x)).join(\"|\") + \")\";\n return joined;\n}\n\n/**\n * @param {RegExp | string} re\n * @returns {number}\n */\nfunction countMatchGroups(re) {\n return (new RegExp(re.toString() + '|')).exec('').length - 1;\n}\n\n/**\n * Does lexeme start with a regular expression match at the beginning\n * @param {RegExp} re\n * @param {string} lexeme\n */\nfunction startsWith(re, lexeme) {\n const match = re && re.exec(lexeme);\n return match && match.index === 0;\n}\n\n// BACKREF_RE matches an open parenthesis or backreference. To avoid\n// an incorrect parse, it additionally matches the following:\n// - [...] elements, where the meaning of parentheses and escapes change\n// - other escape sequences, so we do not misparse escape sequences as\n// interesting elements\n// - non-matching or lookahead parentheses, which do not capture. These\n// follow the '(' with a '?'.\nconst BACKREF_RE = /\\[(?:[^\\\\\\]]|\\\\.)*\\]|\\(\\??|\\\\([1-9][0-9]*)|\\\\./;\n\n// **INTERNAL** Not intended for outside usage\n// join logically computes regexps.join(separator), but fixes the\n// backreferences so they continue to match.\n// it also places each individual regular expression into it's own\n// match group, keeping track of the sequencing of those match groups\n// is currently an exercise for the caller. :-)\n/**\n * @param {(string | RegExp)[]} regexps\n * @param {{joinWith: string}} opts\n * @returns {string}\n */\nfunction _rewriteBackreferences(regexps, { joinWith }) {\n let numCaptures = 0;\n\n return regexps.map((regex) => {\n numCaptures += 1;\n const offset = numCaptures;\n let re = source(regex);\n let out = '';\n\n while (re.length > 0) {\n const match = BACKREF_RE.exec(re);\n if (!match) {\n out += re;\n break;\n }\n out += re.substring(0, match.index);\n re = re.substring(match.index + match[0].length);\n if (match[0][0] === '\\\\' && match[1]) {\n // Adjust the backreference.\n out += '\\\\' + String(Number(match[1]) + offset);\n } else {\n out += match[0];\n if (match[0] === '(') {\n numCaptures++;\n }\n }\n }\n return out;\n }).map(re => `(${re})`).join(joinWith);\n}\n\n/** @typedef {import('highlight.js').Mode} Mode */\n/** @typedef {import('highlight.js').ModeCallback} ModeCallback */\n\n// Common regexps\nconst MATCH_NOTHING_RE = /\\b\\B/;\nconst IDENT_RE = '[a-zA-Z]\\\\w*';\nconst UNDERSCORE_IDENT_RE = '[a-zA-Z_]\\\\w*';\nconst NUMBER_RE = '\\\\b\\\\d+(\\\\.\\\\d+)?';\nconst C_NUMBER_RE = '(-?)(\\\\b0[xX][a-fA-F0-9]+|(\\\\b\\\\d+(\\\\.\\\\d*)?|\\\\.\\\\d+)([eE][-+]?\\\\d+)?)'; // 0x..., 0..., decimal, float\nconst BINARY_NUMBER_RE = '\\\\b(0b[01]+)'; // 0b...\nconst RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\\\*|\\\\*=|\\\\+|\\\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\\\?|\\\\[|\\\\{|\\\\(|\\\\^|\\\\^=|\\\\||\\\\|=|\\\\|\\\\||~';\n\n/**\n* @param { Partial & {binary?: string | RegExp} } opts\n*/\nconst SHEBANG = (opts = {}) => {\n const beginShebang = /^#![ ]*\\//;\n if (opts.binary) {\n opts.begin = concat(\n beginShebang,\n /.*\\b/,\n opts.binary,\n /\\b.*/);\n }\n return inherit$1({\n scope: 'meta',\n begin: beginShebang,\n end: /$/,\n relevance: 0,\n /** @type {ModeCallback} */\n \"on:begin\": (m, resp) => {\n if (m.index !== 0) resp.ignoreMatch();\n }\n }, opts);\n};\n\n// Common modes\nconst BACKSLASH_ESCAPE = {\n begin: '\\\\\\\\[\\\\s\\\\S]', relevance: 0\n};\nconst APOS_STRING_MODE = {\n scope: 'string',\n begin: '\\'',\n end: '\\'',\n illegal: '\\\\n',\n contains: [BACKSLASH_ESCAPE]\n};\nconst QUOTE_STRING_MODE = {\n scope: 'string',\n begin: '\"',\n end: '\"',\n illegal: '\\\\n',\n contains: [BACKSLASH_ESCAPE]\n};\nconst PHRASAL_WORDS_MODE = {\n begin: /\\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\\b/\n};\n/**\n * Creates a comment mode\n *\n * @param {string | RegExp} begin\n * @param {string | RegExp} end\n * @param {Mode | {}} [modeOptions]\n * @returns {Partial}\n */\nconst COMMENT = function(begin, end, modeOptions = {}) {\n const mode = inherit$1(\n {\n scope: 'comment',\n begin,\n end,\n contains: []\n },\n modeOptions\n );\n mode.contains.push({\n scope: 'doctag',\n // hack to avoid the space from being included. the space is necessary to\n // match here to prevent the plain text rule below from gobbling up doctags\n begin: '[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)',\n end: /(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,\n excludeBegin: true,\n relevance: 0\n });\n const ENGLISH_WORD = either(\n // list of common 1 and 2 letter words in English\n \"I\",\n \"a\",\n \"is\",\n \"so\",\n \"us\",\n \"to\",\n \"at\",\n \"if\",\n \"in\",\n \"it\",\n \"on\",\n // note: this is not an exhaustive list of contractions, just popular ones\n /[A-Za-z]+['](d|ve|re|ll|t|s|n)/, // contractions - can't we'd they're let's, etc\n /[A-Za-z]+[-][a-z]+/, // `no-way`, etc.\n /[A-Za-z][a-z]{2,}/ // allow capitalized words at beginning of sentences\n );\n // looking like plain text, more likely to be a comment\n mode.contains.push(\n {\n // TODO: how to include \", (, ) without breaking grammars that use these for\n // comment delimiters?\n // begin: /[ ]+([()\"]?([A-Za-z'-]{3,}|is|a|I|so|us|[tT][oO]|at|if|in|it|on)[.]?[()\":]?([.][ ]|[ ]|\\))){3}/\n // ---\n\n // this tries to find sequences of 3 english words in a row (without any\n // \"programming\" type syntax) this gives us a strong signal that we've\n // TRULY found a comment - vs perhaps scanning with the wrong language.\n // It's possible to find something that LOOKS like the start of the\n // comment - but then if there is no readable text - good chance it is a\n // false match and not a comment.\n //\n // for a visual example please see:\n // https://github.com/highlightjs/highlight.js/issues/2827\n\n begin: concat(\n /[ ]+/, // necessary to prevent us gobbling up doctags like /* @author Bob Mcgill */\n '(',\n ENGLISH_WORD,\n /[.]?[:]?([.][ ]|[ ])/,\n '){3}') // look for 3 words in a row\n }\n );\n return mode;\n};\nconst C_LINE_COMMENT_MODE = COMMENT('//', '$');\nconst C_BLOCK_COMMENT_MODE = COMMENT('/\\\\*', '\\\\*/');\nconst HASH_COMMENT_MODE = COMMENT('#', '$');\nconst NUMBER_MODE = {\n scope: 'number',\n begin: NUMBER_RE,\n relevance: 0\n};\nconst C_NUMBER_MODE = {\n scope: 'number',\n begin: C_NUMBER_RE,\n relevance: 0\n};\nconst BINARY_NUMBER_MODE = {\n scope: 'number',\n begin: BINARY_NUMBER_RE,\n relevance: 0\n};\nconst REGEXP_MODE = {\n scope: \"regexp\",\n begin: /\\/(?=[^/\\n]*\\/)/,\n end: /\\/[gimuy]*/,\n contains: [\n BACKSLASH_ESCAPE,\n {\n begin: /\\[/,\n end: /\\]/,\n relevance: 0,\n contains: [BACKSLASH_ESCAPE]\n }\n ]\n};\nconst TITLE_MODE = {\n scope: 'title',\n begin: IDENT_RE,\n relevance: 0\n};\nconst UNDERSCORE_TITLE_MODE = {\n scope: 'title',\n begin: UNDERSCORE_IDENT_RE,\n relevance: 0\n};\nconst METHOD_GUARD = {\n // excludes method names from keyword processing\n begin: '\\\\.\\\\s*' + UNDERSCORE_IDENT_RE,\n relevance: 0\n};\n\n/**\n * Adds end same as begin mechanics to a mode\n *\n * Your mode must include at least a single () match group as that first match\n * group is what is used for comparison\n * @param {Partial} mode\n */\nconst END_SAME_AS_BEGIN = function(mode) {\n return Object.assign(mode,\n {\n /** @type {ModeCallback} */\n 'on:begin': (m, resp) => { resp.data._beginMatch = m[1]; },\n /** @type {ModeCallback} */\n 'on:end': (m, resp) => { if (resp.data._beginMatch !== m[1]) resp.ignoreMatch(); }\n });\n};\n\nvar MODES = /*#__PURE__*/Object.freeze({\n __proto__: null,\n APOS_STRING_MODE: APOS_STRING_MODE,\n BACKSLASH_ESCAPE: BACKSLASH_ESCAPE,\n BINARY_NUMBER_MODE: BINARY_NUMBER_MODE,\n BINARY_NUMBER_RE: BINARY_NUMBER_RE,\n COMMENT: COMMENT,\n C_BLOCK_COMMENT_MODE: C_BLOCK_COMMENT_MODE,\n C_LINE_COMMENT_MODE: C_LINE_COMMENT_MODE,\n C_NUMBER_MODE: C_NUMBER_MODE,\n C_NUMBER_RE: C_NUMBER_RE,\n END_SAME_AS_BEGIN: END_SAME_AS_BEGIN,\n HASH_COMMENT_MODE: HASH_COMMENT_MODE,\n IDENT_RE: IDENT_RE,\n MATCH_NOTHING_RE: MATCH_NOTHING_RE,\n METHOD_GUARD: METHOD_GUARD,\n NUMBER_MODE: NUMBER_MODE,\n NUMBER_RE: NUMBER_RE,\n PHRASAL_WORDS_MODE: PHRASAL_WORDS_MODE,\n QUOTE_STRING_MODE: QUOTE_STRING_MODE,\n REGEXP_MODE: REGEXP_MODE,\n RE_STARTERS_RE: RE_STARTERS_RE,\n SHEBANG: SHEBANG,\n TITLE_MODE: TITLE_MODE,\n UNDERSCORE_IDENT_RE: UNDERSCORE_IDENT_RE,\n UNDERSCORE_TITLE_MODE: UNDERSCORE_TITLE_MODE\n});\n\n/**\n@typedef {import('highlight.js').CallbackResponse} CallbackResponse\n@typedef {import('highlight.js').CompilerExt} CompilerExt\n*/\n\n// Grammar extensions / plugins\n// See: https://github.com/highlightjs/highlight.js/issues/2833\n\n// Grammar extensions allow \"syntactic sugar\" to be added to the grammar modes\n// without requiring any underlying changes to the compiler internals.\n\n// `compileMatch` being the perfect small example of now allowing a grammar\n// author to write `match` when they desire to match a single expression rather\n// than being forced to use `begin`. The extension then just moves `match` into\n// `begin` when it runs. Ie, no features have been added, but we've just made\n// the experience of writing (and reading grammars) a little bit nicer.\n\n// ------\n\n// TODO: We need negative look-behind support to do this properly\n/**\n * Skip a match if it has a preceding dot\n *\n * This is used for `beginKeywords` to prevent matching expressions such as\n * `bob.keyword.do()`. The mode compiler automatically wires this up as a\n * special _internal_ 'on:begin' callback for modes with `beginKeywords`\n * @param {RegExpMatchArray} match\n * @param {CallbackResponse} response\n */\nfunction skipIfHasPrecedingDot(match, response) {\n const before = match.input[match.index - 1];\n if (before === \".\") {\n response.ignoreMatch();\n }\n}\n\n/**\n *\n * @type {CompilerExt}\n */\nfunction scopeClassName(mode, _parent) {\n // eslint-disable-next-line no-undefined\n if (mode.className !== undefined) {\n mode.scope = mode.className;\n delete mode.className;\n }\n}\n\n/**\n * `beginKeywords` syntactic sugar\n * @type {CompilerExt}\n */\nfunction beginKeywords(mode, parent) {\n if (!parent) return;\n if (!mode.beginKeywords) return;\n\n // for languages with keywords that include non-word characters checking for\n // a word boundary is not sufficient, so instead we check for a word boundary\n // or whitespace - this does no harm in any case since our keyword engine\n // doesn't allow spaces in keywords anyways and we still check for the boundary\n // first\n mode.begin = '\\\\b(' + mode.beginKeywords.split(' ').join('|') + ')(?!\\\\.)(?=\\\\b|\\\\s)';\n mode.__beforeBegin = skipIfHasPrecedingDot;\n mode.keywords = mode.keywords || mode.beginKeywords;\n delete mode.beginKeywords;\n\n // prevents double relevance, the keywords themselves provide\n // relevance, the mode doesn't need to double it\n // eslint-disable-next-line no-undefined\n if (mode.relevance === undefined) mode.relevance = 0;\n}\n\n/**\n * Allow `illegal` to contain an array of illegal values\n * @type {CompilerExt}\n */\nfunction compileIllegal(mode, _parent) {\n if (!Array.isArray(mode.illegal)) return;\n\n mode.illegal = either(...mode.illegal);\n}\n\n/**\n * `match` to match a single expression for readability\n * @type {CompilerExt}\n */\nfunction compileMatch(mode, _parent) {\n if (!mode.match) return;\n if (mode.begin || mode.end) throw new Error(\"begin & end are not supported with match\");\n\n mode.begin = mode.match;\n delete mode.match;\n}\n\n/**\n * provides the default 1 relevance to all modes\n * @type {CompilerExt}\n */\nfunction compileRelevance(mode, _parent) {\n // eslint-disable-next-line no-undefined\n if (mode.relevance === undefined) mode.relevance = 1;\n}\n\n// allow beforeMatch to act as a \"qualifier\" for the match\n// the full match begin must be [beforeMatch][begin]\nconst beforeMatchExt = (mode, parent) => {\n if (!mode.beforeMatch) return;\n // starts conflicts with endsParent which we need to make sure the child\n // rule is not matched multiple times\n if (mode.starts) throw new Error(\"beforeMatch cannot be used with starts\");\n\n const originalMode = Object.assign({}, mode);\n Object.keys(mode).forEach((key) => { delete mode[key]; });\n\n mode.keywords = originalMode.keywords;\n mode.begin = concat(originalMode.beforeMatch, lookahead(originalMode.begin));\n mode.starts = {\n relevance: 0,\n contains: [\n Object.assign(originalMode, { endsParent: true })\n ]\n };\n mode.relevance = 0;\n\n delete originalMode.beforeMatch;\n};\n\n// keywords that should have no default relevance value\nconst COMMON_KEYWORDS = [\n 'of',\n 'and',\n 'for',\n 'in',\n 'not',\n 'or',\n 'if',\n 'then',\n 'parent', // common variable name\n 'list', // common variable name\n 'value' // common variable name\n];\n\nconst DEFAULT_KEYWORD_SCOPE = \"keyword\";\n\n/**\n * Given raw keywords from a language definition, compile them.\n *\n * @param {string | Record | Array} rawKeywords\n * @param {boolean} caseInsensitive\n */\nfunction compileKeywords(rawKeywords, caseInsensitive, scopeName = DEFAULT_KEYWORD_SCOPE) {\n /** @type {import(\"highlight.js/private\").KeywordDict} */\n const compiledKeywords = Object.create(null);\n\n // input can be a string of keywords, an array of keywords, or a object with\n // named keys representing scopeName (which can then point to a string or array)\n if (typeof rawKeywords === 'string') {\n compileList(scopeName, rawKeywords.split(\" \"));\n } else if (Array.isArray(rawKeywords)) {\n compileList(scopeName, rawKeywords);\n } else {\n Object.keys(rawKeywords).forEach(function(scopeName) {\n // collapse all our objects back into the parent object\n Object.assign(\n compiledKeywords,\n compileKeywords(rawKeywords[scopeName], caseInsensitive, scopeName)\n );\n });\n }\n return compiledKeywords;\n\n // ---\n\n /**\n * Compiles an individual list of keywords\n *\n * Ex: \"for if when while|5\"\n *\n * @param {string} scopeName\n * @param {Array} keywordList\n */\n function compileList(scopeName, keywordList) {\n if (caseInsensitive) {\n keywordList = keywordList.map(x => x.toLowerCase());\n }\n keywordList.forEach(function(keyword) {\n const pair = keyword.split('|');\n compiledKeywords[pair[0]] = [scopeName, scoreForKeyword(pair[0], pair[1])];\n });\n }\n}\n\n/**\n * Returns the proper score for a given keyword\n *\n * Also takes into account comment keywords, which will be scored 0 UNLESS\n * another score has been manually assigned.\n * @param {string} keyword\n * @param {string} [providedScore]\n */\nfunction scoreForKeyword(keyword, providedScore) {\n // manual scores always win over common keywords\n // so you can force a score of 1 if you really insist\n if (providedScore) {\n return Number(providedScore);\n }\n\n return commonKeyword(keyword) ? 0 : 1;\n}\n\n/**\n * Determines if a given keyword is common or not\n *\n * @param {string} keyword */\nfunction commonKeyword(keyword) {\n return COMMON_KEYWORDS.includes(keyword.toLowerCase());\n}\n\n/*\n\nFor the reasoning behind this please see:\nhttps://github.com/highlightjs/highlight.js/issues/2880#issuecomment-747275419\n\n*/\n\n/**\n * @type {Record}\n */\nconst seenDeprecations = {};\n\n/**\n * @param {string} message\n */\nconst error = (message) => {\n console.error(message);\n};\n\n/**\n * @param {string} message\n * @param {any} args\n */\nconst warn = (message, ...args) => {\n console.log(`WARN: ${message}`, ...args);\n};\n\n/**\n * @param {string} version\n * @param {string} message\n */\nconst deprecated = (version, message) => {\n if (seenDeprecations[`${version}/${message}`]) return;\n\n console.log(`Deprecated as of ${version}. ${message}`);\n seenDeprecations[`${version}/${message}`] = true;\n};\n\n/* eslint-disable no-throw-literal */\n\n/**\n@typedef {import('highlight.js').CompiledMode} CompiledMode\n*/\n\nconst MultiClassError = new Error();\n\n/**\n * Renumbers labeled scope names to account for additional inner match\n * groups that otherwise would break everything.\n *\n * Lets say we 3 match scopes:\n *\n * { 1 => ..., 2 => ..., 3 => ... }\n *\n * So what we need is a clean match like this:\n *\n * (a)(b)(c) => [ \"a\", \"b\", \"c\" ]\n *\n * But this falls apart with inner match groups:\n *\n * (a)(((b)))(c) => [\"a\", \"b\", \"b\", \"b\", \"c\" ]\n *\n * Our scopes are now \"out of alignment\" and we're repeating `b` 3 times.\n * What needs to happen is the numbers are remapped:\n *\n * { 1 => ..., 2 => ..., 5 => ... }\n *\n * We also need to know that the ONLY groups that should be output\n * are 1, 2, and 5. This function handles this behavior.\n *\n * @param {CompiledMode} mode\n * @param {Array} regexes\n * @param {{key: \"beginScope\"|\"endScope\"}} opts\n */\nfunction remapScopeNames(mode, regexes, { key }) {\n let offset = 0;\n const scopeNames = mode[key];\n /** @type Record */\n const emit = {};\n /** @type Record */\n const positions = {};\n\n for (let i = 1; i <= regexes.length; i++) {\n positions[i + offset] = scopeNames[i];\n emit[i + offset] = true;\n offset += countMatchGroups(regexes[i - 1]);\n }\n // we use _emit to keep track of which match groups are \"top-level\" to avoid double\n // output from inside match groups\n mode[key] = positions;\n mode[key]._emit = emit;\n mode[key]._multi = true;\n}\n\n/**\n * @param {CompiledMode} mode\n */\nfunction beginMultiClass(mode) {\n if (!Array.isArray(mode.begin)) return;\n\n if (mode.skip || mode.excludeBegin || mode.returnBegin) {\n error(\"skip, excludeBegin, returnBegin not compatible with beginScope: {}\");\n throw MultiClassError;\n }\n\n if (typeof mode.beginScope !== \"object\" || mode.beginScope === null) {\n error(\"beginScope must be object\");\n throw MultiClassError;\n }\n\n remapScopeNames(mode, mode.begin, { key: \"beginScope\" });\n mode.begin = _rewriteBackreferences(mode.begin, { joinWith: \"\" });\n}\n\n/**\n * @param {CompiledMode} mode\n */\nfunction endMultiClass(mode) {\n if (!Array.isArray(mode.end)) return;\n\n if (mode.skip || mode.excludeEnd || mode.returnEnd) {\n error(\"skip, excludeEnd, returnEnd not compatible with endScope: {}\");\n throw MultiClassError;\n }\n\n if (typeof mode.endScope !== \"object\" || mode.endScope === null) {\n error(\"endScope must be object\");\n throw MultiClassError;\n }\n\n remapScopeNames(mode, mode.end, { key: \"endScope\" });\n mode.end = _rewriteBackreferences(mode.end, { joinWith: \"\" });\n}\n\n/**\n * this exists only to allow `scope: {}` to be used beside `match:`\n * Otherwise `beginScope` would necessary and that would look weird\n\n {\n match: [ /def/, /\\w+/ ]\n scope: { 1: \"keyword\" , 2: \"title\" }\n }\n\n * @param {CompiledMode} mode\n */\nfunction scopeSugar(mode) {\n if (mode.scope && typeof mode.scope === \"object\" && mode.scope !== null) {\n mode.beginScope = mode.scope;\n delete mode.scope;\n }\n}\n\n/**\n * @param {CompiledMode} mode\n */\nfunction MultiClass(mode) {\n scopeSugar(mode);\n\n if (typeof mode.beginScope === \"string\") {\n mode.beginScope = { _wrap: mode.beginScope };\n }\n if (typeof mode.endScope === \"string\") {\n mode.endScope = { _wrap: mode.endScope };\n }\n\n beginMultiClass(mode);\n endMultiClass(mode);\n}\n\n/**\n@typedef {import('highlight.js').Mode} Mode\n@typedef {import('highlight.js').CompiledMode} CompiledMode\n@typedef {import('highlight.js').Language} Language\n@typedef {import('highlight.js').HLJSPlugin} HLJSPlugin\n@typedef {import('highlight.js').CompiledLanguage} CompiledLanguage\n*/\n\n// compilation\n\n/**\n * Compiles a language definition result\n *\n * Given the raw result of a language definition (Language), compiles this so\n * that it is ready for highlighting code.\n * @param {Language} language\n * @returns {CompiledLanguage}\n */\nfunction compileLanguage(language) {\n /**\n * Builds a regex with the case sensitivity of the current language\n *\n * @param {RegExp | string} value\n * @param {boolean} [global]\n */\n function langRe(value, global) {\n return new RegExp(\n source(value),\n 'm'\n + (language.case_insensitive ? 'i' : '')\n + (language.unicodeRegex ? 'u' : '')\n + (global ? 'g' : '')\n );\n }\n\n /**\n Stores multiple regular expressions and allows you to quickly search for\n them all in a string simultaneously - returning the first match. It does\n this by creating a huge (a|b|c) regex - each individual item wrapped with ()\n and joined by `|` - using match groups to track position. When a match is\n found checking which position in the array has content allows us to figure\n out which of the original regexes / match groups triggered the match.\n\n The match object itself (the result of `Regex.exec`) is returned but also\n enhanced by merging in any meta-data that was registered with the regex.\n This is how we keep track of which mode matched, and what type of rule\n (`illegal`, `begin`, end, etc).\n */\n class MultiRegex {\n constructor() {\n this.matchIndexes = {};\n // @ts-ignore\n this.regexes = [];\n this.matchAt = 1;\n this.position = 0;\n }\n\n // @ts-ignore\n addRule(re, opts) {\n opts.position = this.position++;\n // @ts-ignore\n this.matchIndexes[this.matchAt] = opts;\n this.regexes.push([opts, re]);\n this.matchAt += countMatchGroups(re) + 1;\n }\n\n compile() {\n if (this.regexes.length === 0) {\n // avoids the need to check length every time exec is called\n // @ts-ignore\n this.exec = () => null;\n }\n const terminators = this.regexes.map(el => el[1]);\n this.matcherRe = langRe(_rewriteBackreferences(terminators, { joinWith: '|' }), true);\n this.lastIndex = 0;\n }\n\n /** @param {string} s */\n exec(s) {\n this.matcherRe.lastIndex = this.lastIndex;\n const match = this.matcherRe.exec(s);\n if (!match) { return null; }\n\n // eslint-disable-next-line no-undefined\n const i = match.findIndex((el, i) => i > 0 && el !== undefined);\n // @ts-ignore\n const matchData = this.matchIndexes[i];\n // trim off any earlier non-relevant match groups (ie, the other regex\n // match groups that make up the multi-matcher)\n match.splice(0, i);\n\n return Object.assign(match, matchData);\n }\n }\n\n /*\n Created to solve the key deficiently with MultiRegex - there is no way to\n test for multiple matches at a single location. Why would we need to do\n that? In the future a more dynamic engine will allow certain matches to be\n ignored. An example: if we matched say the 3rd regex in a large group but\n decided to ignore it - we'd need to started testing again at the 4th\n regex... but MultiRegex itself gives us no real way to do that.\n\n So what this class creates MultiRegexs on the fly for whatever search\n position they are needed.\n\n NOTE: These additional MultiRegex objects are created dynamically. For most\n grammars most of the time we will never actually need anything more than the\n first MultiRegex - so this shouldn't have too much overhead.\n\n Say this is our search group, and we match regex3, but wish to ignore it.\n\n regex1 | regex2 | regex3 | regex4 | regex5 ' ie, startAt = 0\n\n What we need is a new MultiRegex that only includes the remaining\n possibilities:\n\n regex4 | regex5 ' ie, startAt = 3\n\n This class wraps all that complexity up in a simple API... `startAt` decides\n where in the array of expressions to start doing the matching. It\n auto-increments, so if a match is found at position 2, then startAt will be\n set to 3. If the end is reached startAt will return to 0.\n\n MOST of the time the parser will be setting startAt manually to 0.\n */\n class ResumableMultiRegex {\n constructor() {\n // @ts-ignore\n this.rules = [];\n // @ts-ignore\n this.multiRegexes = [];\n this.count = 0;\n\n this.lastIndex = 0;\n this.regexIndex = 0;\n }\n\n // @ts-ignore\n getMatcher(index) {\n if (this.multiRegexes[index]) return this.multiRegexes[index];\n\n const matcher = new MultiRegex();\n this.rules.slice(index).forEach(([re, opts]) => matcher.addRule(re, opts));\n matcher.compile();\n this.multiRegexes[index] = matcher;\n return matcher;\n }\n\n resumingScanAtSamePosition() {\n return this.regexIndex !== 0;\n }\n\n considerAll() {\n this.regexIndex = 0;\n }\n\n // @ts-ignore\n addRule(re, opts) {\n this.rules.push([re, opts]);\n if (opts.type === \"begin\") this.count++;\n }\n\n /** @param {string} s */\n exec(s) {\n const m = this.getMatcher(this.regexIndex);\n m.lastIndex = this.lastIndex;\n let result = m.exec(s);\n\n // The following is because we have no easy way to say \"resume scanning at the\n // existing position but also skip the current rule ONLY\". What happens is\n // all prior rules are also skipped which can result in matching the wrong\n // thing. Example of matching \"booger\":\n\n // our matcher is [string, \"booger\", number]\n //\n // ....booger....\n\n // if \"booger\" is ignored then we'd really need a regex to scan from the\n // SAME position for only: [string, number] but ignoring \"booger\" (if it\n // was the first match), a simple resume would scan ahead who knows how\n // far looking only for \"number\", ignoring potential string matches (or\n // future \"booger\" matches that might be valid.)\n\n // So what we do: We execute two matchers, one resuming at the same\n // position, but the second full matcher starting at the position after:\n\n // /--- resume first regex match here (for [number])\n // |/---- full match here for [string, \"booger\", number]\n // vv\n // ....booger....\n\n // Which ever results in a match first is then used. So this 3-4 step\n // process essentially allows us to say \"match at this position, excluding\n // a prior rule that was ignored\".\n //\n // 1. Match \"booger\" first, ignore. Also proves that [string] does non match.\n // 2. Resume matching for [number]\n // 3. Match at index + 1 for [string, \"booger\", number]\n // 4. If #2 and #3 result in matches, which came first?\n if (this.resumingScanAtSamePosition()) {\n if (result && result.index === this.lastIndex) ; else { // use the second matcher result\n const m2 = this.getMatcher(0);\n m2.lastIndex = this.lastIndex + 1;\n result = m2.exec(s);\n }\n }\n\n if (result) {\n this.regexIndex += result.position + 1;\n if (this.regexIndex === this.count) {\n // wrap-around to considering all matches again\n this.considerAll();\n }\n }\n\n return result;\n }\n }\n\n /**\n * Given a mode, builds a huge ResumableMultiRegex that can be used to walk\n * the content and find matches.\n *\n * @param {CompiledMode} mode\n * @returns {ResumableMultiRegex}\n */\n function buildModeRegex(mode) {\n const mm = new ResumableMultiRegex();\n\n mode.contains.forEach(term => mm.addRule(term.begin, { rule: term, type: \"begin\" }));\n\n if (mode.terminatorEnd) {\n mm.addRule(mode.terminatorEnd, { type: \"end\" });\n }\n if (mode.illegal) {\n mm.addRule(mode.illegal, { type: \"illegal\" });\n }\n\n return mm;\n }\n\n /** skip vs abort vs ignore\n *\n * @skip - The mode is still entered and exited normally (and contains rules apply),\n * but all content is held and added to the parent buffer rather than being\n * output when the mode ends. Mostly used with `sublanguage` to build up\n * a single large buffer than can be parsed by sublanguage.\n *\n * - The mode begin ands ends normally.\n * - Content matched is added to the parent mode buffer.\n * - The parser cursor is moved forward normally.\n *\n * @abort - A hack placeholder until we have ignore. Aborts the mode (as if it\n * never matched) but DOES NOT continue to match subsequent `contains`\n * modes. Abort is bad/suboptimal because it can result in modes\n * farther down not getting applied because an earlier rule eats the\n * content but then aborts.\n *\n * - The mode does not begin.\n * - Content matched by `begin` is added to the mode buffer.\n * - The parser cursor is moved forward accordingly.\n *\n * @ignore - Ignores the mode (as if it never matched) and continues to match any\n * subsequent `contains` modes. Ignore isn't technically possible with\n * the current parser implementation.\n *\n * - The mode does not begin.\n * - Content matched by `begin` is ignored.\n * - The parser cursor is not moved forward.\n */\n\n /**\n * Compiles an individual mode\n *\n * This can raise an error if the mode contains certain detectable known logic\n * issues.\n * @param {Mode} mode\n * @param {CompiledMode | null} [parent]\n * @returns {CompiledMode | never}\n */\n function compileMode(mode, parent) {\n const cmode = /** @type CompiledMode */ (mode);\n if (mode.isCompiled) return cmode;\n\n [\n scopeClassName,\n // do this early so compiler extensions generally don't have to worry about\n // the distinction between match/begin\n compileMatch,\n MultiClass,\n beforeMatchExt\n ].forEach(ext => ext(mode, parent));\n\n language.compilerExtensions.forEach(ext => ext(mode, parent));\n\n // __beforeBegin is considered private API, internal use only\n mode.__beforeBegin = null;\n\n [\n beginKeywords,\n // do this later so compiler extensions that come earlier have access to the\n // raw array if they wanted to perhaps manipulate it, etc.\n compileIllegal,\n // default to 1 relevance if not specified\n compileRelevance\n ].forEach(ext => ext(mode, parent));\n\n mode.isCompiled = true;\n\n let keywordPattern = null;\n if (typeof mode.keywords === \"object\" && mode.keywords.$pattern) {\n // we need a copy because keywords might be compiled multiple times\n // so we can't go deleting $pattern from the original on the first\n // pass\n mode.keywords = Object.assign({}, mode.keywords);\n keywordPattern = mode.keywords.$pattern;\n delete mode.keywords.$pattern;\n }\n keywordPattern = keywordPattern || /\\w+/;\n\n if (mode.keywords) {\n mode.keywords = compileKeywords(mode.keywords, language.case_insensitive);\n }\n\n cmode.keywordPatternRe = langRe(keywordPattern, true);\n\n if (parent) {\n if (!mode.begin) mode.begin = /\\B|\\b/;\n cmode.beginRe = langRe(cmode.begin);\n if (!mode.end && !mode.endsWithParent) mode.end = /\\B|\\b/;\n if (mode.end) cmode.endRe = langRe(cmode.end);\n cmode.terminatorEnd = source(cmode.end) || '';\n if (mode.endsWithParent && parent.terminatorEnd) {\n cmode.terminatorEnd += (mode.end ? '|' : '') + parent.terminatorEnd;\n }\n }\n if (mode.illegal) cmode.illegalRe = langRe(/** @type {RegExp | string} */ (mode.illegal));\n if (!mode.contains) mode.contains = [];\n\n mode.contains = [].concat(...mode.contains.map(function(c) {\n return expandOrCloneMode(c === 'self' ? mode : c);\n }));\n mode.contains.forEach(function(c) { compileMode(/** @type Mode */ (c), cmode); });\n\n if (mode.starts) {\n compileMode(mode.starts, parent);\n }\n\n cmode.matcher = buildModeRegex(cmode);\n return cmode;\n }\n\n if (!language.compilerExtensions) language.compilerExtensions = [];\n\n // self is not valid at the top-level\n if (language.contains && language.contains.includes('self')) {\n throw new Error(\"ERR: contains `self` is not supported at the top-level of a language. See documentation.\");\n }\n\n // we need a null object, which inherit will guarantee\n language.classNameAliases = inherit$1(language.classNameAliases || {});\n\n return compileMode(/** @type Mode */ (language));\n}\n\n/**\n * Determines if a mode has a dependency on it's parent or not\n *\n * If a mode does have a parent dependency then often we need to clone it if\n * it's used in multiple places so that each copy points to the correct parent,\n * where-as modes without a parent can often safely be re-used at the bottom of\n * a mode chain.\n *\n * @param {Mode | null} mode\n * @returns {boolean} - is there a dependency on the parent?\n * */\nfunction dependencyOnParent(mode) {\n if (!mode) return false;\n\n return mode.endsWithParent || dependencyOnParent(mode.starts);\n}\n\n/**\n * Expands a mode or clones it if necessary\n *\n * This is necessary for modes with parental dependenceis (see notes on\n * `dependencyOnParent`) and for nodes that have `variants` - which must then be\n * exploded into their own individual modes at compile time.\n *\n * @param {Mode} mode\n * @returns {Mode | Mode[]}\n * */\nfunction expandOrCloneMode(mode) {\n if (mode.variants && !mode.cachedVariants) {\n mode.cachedVariants = mode.variants.map(function(variant) {\n return inherit$1(mode, { variants: null }, variant);\n });\n }\n\n // EXPAND\n // if we have variants then essentially \"replace\" the mode with the variants\n // this happens in compileMode, where this function is called from\n if (mode.cachedVariants) {\n return mode.cachedVariants;\n }\n\n // CLONE\n // if we have dependencies on parents then we need a unique\n // instance of ourselves, so we can be reused with many\n // different parents without issue\n if (dependencyOnParent(mode)) {\n return inherit$1(mode, { starts: mode.starts ? inherit$1(mode.starts) : null });\n }\n\n if (Object.isFrozen(mode)) {\n return inherit$1(mode);\n }\n\n // no special dependency issues, just return ourselves\n return mode;\n}\n\nvar version = \"11.11.1\";\n\nclass HTMLInjectionError extends Error {\n constructor(reason, html) {\n super(reason);\n this.name = \"HTMLInjectionError\";\n this.html = html;\n }\n}\n\n/*\nSyntax highlighting with language autodetection.\nhttps://highlightjs.org/\n*/\n\n\n\n/**\n@typedef {import('highlight.js').Mode} Mode\n@typedef {import('highlight.js').CompiledMode} CompiledMode\n@typedef {import('highlight.js').CompiledScope} CompiledScope\n@typedef {import('highlight.js').Language} Language\n@typedef {import('highlight.js').HLJSApi} HLJSApi\n@typedef {import('highlight.js').HLJSPlugin} HLJSPlugin\n@typedef {import('highlight.js').PluginEvent} PluginEvent\n@typedef {import('highlight.js').HLJSOptions} HLJSOptions\n@typedef {import('highlight.js').LanguageFn} LanguageFn\n@typedef {import('highlight.js').HighlightedHTMLElement} HighlightedHTMLElement\n@typedef {import('highlight.js').BeforeHighlightContext} BeforeHighlightContext\n@typedef {import('highlight.js/private').MatchType} MatchType\n@typedef {import('highlight.js/private').KeywordData} KeywordData\n@typedef {import('highlight.js/private').EnhancedMatch} EnhancedMatch\n@typedef {import('highlight.js/private').AnnotatedError} AnnotatedError\n@typedef {import('highlight.js').AutoHighlightResult} AutoHighlightResult\n@typedef {import('highlight.js').HighlightOptions} HighlightOptions\n@typedef {import('highlight.js').HighlightResult} HighlightResult\n*/\n\n\nconst escape = escapeHTML;\nconst inherit = inherit$1;\nconst NO_MATCH = Symbol(\"nomatch\");\nconst MAX_KEYWORD_HITS = 7;\n\n/**\n * @param {any} hljs - object that is extended (legacy)\n * @returns {HLJSApi}\n */\nconst HLJS = function(hljs) {\n // Global internal variables used within the highlight.js library.\n /** @type {Record} */\n const languages = Object.create(null);\n /** @type {Record} */\n const aliases = Object.create(null);\n /** @type {HLJSPlugin[]} */\n const plugins = [];\n\n // safe/production mode - swallows more errors, tries to keep running\n // even if a single syntax or parse hits a fatal error\n let SAFE_MODE = true;\n const LANGUAGE_NOT_FOUND = \"Could not find the language '{}', did you forget to load/include a language module?\";\n /** @type {Language} */\n const PLAINTEXT_LANGUAGE = { disableAutodetect: true, name: 'Plain text', contains: [] };\n\n // Global options used when within external APIs. This is modified when\n // calling the `hljs.configure` function.\n /** @type HLJSOptions */\n let options = {\n ignoreUnescapedHTML: false,\n throwUnescapedHTML: false,\n noHighlightRe: /^(no-?highlight)$/i,\n languageDetectRe: /\\blang(?:uage)?-([\\w-]+)\\b/i,\n classPrefix: 'hljs-',\n cssSelector: 'pre code',\n languages: null,\n // beta configuration options, subject to change, welcome to discuss\n // https://github.com/highlightjs/highlight.js/issues/1086\n __emitter: TokenTreeEmitter\n };\n\n /* Utility functions */\n\n /**\n * Tests a language name to see if highlighting should be skipped\n * @param {string} languageName\n */\n function shouldNotHighlight(languageName) {\n return options.noHighlightRe.test(languageName);\n }\n\n /**\n * @param {HighlightedHTMLElement} block - the HTML element to determine language for\n */\n function blockLanguage(block) {\n let classes = block.className + ' ';\n\n classes += block.parentNode ? block.parentNode.className : '';\n\n // language-* takes precedence over non-prefixed class names.\n const match = options.languageDetectRe.exec(classes);\n if (match) {\n const language = getLanguage(match[1]);\n if (!language) {\n warn(LANGUAGE_NOT_FOUND.replace(\"{}\", match[1]));\n warn(\"Falling back to no-highlight mode for this block.\", block);\n }\n return language ? match[1] : 'no-highlight';\n }\n\n return classes\n .split(/\\s+/)\n .find((_class) => shouldNotHighlight(_class) || getLanguage(_class));\n }\n\n /**\n * Core highlighting function.\n *\n * OLD API\n * highlight(lang, code, ignoreIllegals, continuation)\n *\n * NEW API\n * highlight(code, {lang, ignoreIllegals})\n *\n * @param {string} codeOrLanguageName - the language to use for highlighting\n * @param {string | HighlightOptions} optionsOrCode - the code to highlight\n * @param {boolean} [ignoreIllegals] - whether to ignore illegal matches, default is to bail\n *\n * @returns {HighlightResult} Result - an object that represents the result\n * @property {string} language - the language name\n * @property {number} relevance - the relevance score\n * @property {string} value - the highlighted HTML code\n * @property {string} code - the original raw code\n * @property {CompiledMode} top - top of the current mode stack\n * @property {boolean} illegal - indicates whether any illegal matches were found\n */\n function highlight(codeOrLanguageName, optionsOrCode, ignoreIllegals) {\n let code = \"\";\n let languageName = \"\";\n if (typeof optionsOrCode === \"object\") {\n code = codeOrLanguageName;\n ignoreIllegals = optionsOrCode.ignoreIllegals;\n languageName = optionsOrCode.language;\n } else {\n // old API\n deprecated(\"10.7.0\", \"highlight(lang, code, ...args) has been deprecated.\");\n deprecated(\"10.7.0\", \"Please use highlight(code, options) instead.\\nhttps://github.com/highlightjs/highlight.js/issues/2277\");\n languageName = codeOrLanguageName;\n code = optionsOrCode;\n }\n\n // https://github.com/highlightjs/highlight.js/issues/3149\n // eslint-disable-next-line no-undefined\n if (ignoreIllegals === undefined) { ignoreIllegals = true; }\n\n /** @type {BeforeHighlightContext} */\n const context = {\n code,\n language: languageName\n };\n // the plugin can change the desired language or the code to be highlighted\n // just be changing the object it was passed\n fire(\"before:highlight\", context);\n\n // a before plugin can usurp the result completely by providing it's own\n // in which case we don't even need to call highlight\n const result = context.result\n ? context.result\n : _highlight(context.language, context.code, ignoreIllegals);\n\n result.code = context.code;\n // the plugin can change anything in result to suite it\n fire(\"after:highlight\", result);\n\n return result;\n }\n\n /**\n * private highlight that's used internally and does not fire callbacks\n *\n * @param {string} languageName - the language to use for highlighting\n * @param {string} codeToHighlight - the code to highlight\n * @param {boolean?} [ignoreIllegals] - whether to ignore illegal matches, default is to bail\n * @param {CompiledMode?} [continuation] - current continuation mode, if any\n * @returns {HighlightResult} - result of the highlight operation\n */\n function _highlight(languageName, codeToHighlight, ignoreIllegals, continuation) {\n const keywordHits = Object.create(null);\n\n /**\n * Return keyword data if a match is a keyword\n * @param {CompiledMode} mode - current mode\n * @param {string} matchText - the textual match\n * @returns {KeywordData | false}\n */\n function keywordData(mode, matchText) {\n return mode.keywords[matchText];\n }\n\n function processKeywords() {\n if (!top.keywords) {\n emitter.addText(modeBuffer);\n return;\n }\n\n let lastIndex = 0;\n top.keywordPatternRe.lastIndex = 0;\n let match = top.keywordPatternRe.exec(modeBuffer);\n let buf = \"\";\n\n while (match) {\n buf += modeBuffer.substring(lastIndex, match.index);\n const word = language.case_insensitive ? match[0].toLowerCase() : match[0];\n const data = keywordData(top, word);\n if (data) {\n const [kind, keywordRelevance] = data;\n emitter.addText(buf);\n buf = \"\";\n\n keywordHits[word] = (keywordHits[word] || 0) + 1;\n if (keywordHits[word] <= MAX_KEYWORD_HITS) relevance += keywordRelevance;\n if (kind.startsWith(\"_\")) {\n // _ implied for relevance only, do not highlight\n // by applying a class name\n buf += match[0];\n } else {\n const cssClass = language.classNameAliases[kind] || kind;\n emitKeyword(match[0], cssClass);\n }\n } else {\n buf += match[0];\n }\n lastIndex = top.keywordPatternRe.lastIndex;\n match = top.keywordPatternRe.exec(modeBuffer);\n }\n buf += modeBuffer.substring(lastIndex);\n emitter.addText(buf);\n }\n\n function processSubLanguage() {\n if (modeBuffer === \"\") return;\n /** @type HighlightResult */\n let result = null;\n\n if (typeof top.subLanguage === 'string') {\n if (!languages[top.subLanguage]) {\n emitter.addText(modeBuffer);\n return;\n }\n result = _highlight(top.subLanguage, modeBuffer, true, continuations[top.subLanguage]);\n continuations[top.subLanguage] = /** @type {CompiledMode} */ (result._top);\n } else {\n result = highlightAuto(modeBuffer, top.subLanguage.length ? top.subLanguage : null);\n }\n\n // Counting embedded language score towards the host language may be disabled\n // with zeroing the containing mode relevance. Use case in point is Markdown that\n // allows XML everywhere and makes every XML snippet to have a much larger Markdown\n // score.\n if (top.relevance > 0) {\n relevance += result.relevance;\n }\n emitter.__addSublanguage(result._emitter, result.language);\n }\n\n function processBuffer() {\n if (top.subLanguage != null) {\n processSubLanguage();\n } else {\n processKeywords();\n }\n modeBuffer = '';\n }\n\n /**\n * @param {string} text\n * @param {string} scope\n */\n function emitKeyword(keyword, scope) {\n if (keyword === \"\") return;\n\n emitter.startScope(scope);\n emitter.addText(keyword);\n emitter.endScope();\n }\n\n /**\n * @param {CompiledScope} scope\n * @param {RegExpMatchArray} match\n */\n function emitMultiClass(scope, match) {\n let i = 1;\n const max = match.length - 1;\n while (i <= max) {\n if (!scope._emit[i]) { i++; continue; }\n const klass = language.classNameAliases[scope[i]] || scope[i];\n const text = match[i];\n if (klass) {\n emitKeyword(text, klass);\n } else {\n modeBuffer = text;\n processKeywords();\n modeBuffer = \"\";\n }\n i++;\n }\n }\n\n /**\n * @param {CompiledMode} mode - new mode to start\n * @param {RegExpMatchArray} match\n */\n function startNewMode(mode, match) {\n if (mode.scope && typeof mode.scope === \"string\") {\n emitter.openNode(language.classNameAliases[mode.scope] || mode.scope);\n }\n if (mode.beginScope) {\n // beginScope just wraps the begin match itself in a scope\n if (mode.beginScope._wrap) {\n emitKeyword(modeBuffer, language.classNameAliases[mode.beginScope._wrap] || mode.beginScope._wrap);\n modeBuffer = \"\";\n } else if (mode.beginScope._multi) {\n // at this point modeBuffer should just be the match\n emitMultiClass(mode.beginScope, match);\n modeBuffer = \"\";\n }\n }\n\n top = Object.create(mode, { parent: { value: top } });\n return top;\n }\n\n /**\n * @param {CompiledMode } mode - the mode to potentially end\n * @param {RegExpMatchArray} match - the latest match\n * @param {string} matchPlusRemainder - match plus remainder of content\n * @returns {CompiledMode | void} - the next mode, or if void continue on in current mode\n */\n function endOfMode(mode, match, matchPlusRemainder) {\n let matched = startsWith(mode.endRe, matchPlusRemainder);\n\n if (matched) {\n if (mode[\"on:end\"]) {\n const resp = new Response(mode);\n mode[\"on:end\"](match, resp);\n if (resp.isMatchIgnored) matched = false;\n }\n\n if (matched) {\n while (mode.endsParent && mode.parent) {\n mode = mode.parent;\n }\n return mode;\n }\n }\n // even if on:end fires an `ignore` it's still possible\n // that we might trigger the end node because of a parent mode\n if (mode.endsWithParent) {\n return endOfMode(mode.parent, match, matchPlusRemainder);\n }\n }\n\n /**\n * Handle matching but then ignoring a sequence of text\n *\n * @param {string} lexeme - string containing full match text\n */\n function doIgnore(lexeme) {\n if (top.matcher.regexIndex === 0) {\n // no more regexes to potentially match here, so we move the cursor forward one\n // space\n modeBuffer += lexeme[0];\n return 1;\n } else {\n // no need to move the cursor, we still have additional regexes to try and\n // match at this very spot\n resumeScanAtSamePosition = true;\n return 0;\n }\n }\n\n /**\n * Handle the start of a new potential mode match\n *\n * @param {EnhancedMatch} match - the current match\n * @returns {number} how far to advance the parse cursor\n */\n function doBeginMatch(match) {\n const lexeme = match[0];\n const newMode = match.rule;\n\n const resp = new Response(newMode);\n // first internal before callbacks, then the public ones\n const beforeCallbacks = [newMode.__beforeBegin, newMode[\"on:begin\"]];\n for (const cb of beforeCallbacks) {\n if (!cb) continue;\n cb(match, resp);\n if (resp.isMatchIgnored) return doIgnore(lexeme);\n }\n\n if (newMode.skip) {\n modeBuffer += lexeme;\n } else {\n if (newMode.excludeBegin) {\n modeBuffer += lexeme;\n }\n processBuffer();\n if (!newMode.returnBegin && !newMode.excludeBegin) {\n modeBuffer = lexeme;\n }\n }\n startNewMode(newMode, match);\n return newMode.returnBegin ? 0 : lexeme.length;\n }\n\n /**\n * Handle the potential end of mode\n *\n * @param {RegExpMatchArray} match - the current match\n */\n function doEndMatch(match) {\n const lexeme = match[0];\n const matchPlusRemainder = codeToHighlight.substring(match.index);\n\n const endMode = endOfMode(top, match, matchPlusRemainder);\n if (!endMode) { return NO_MATCH; }\n\n const origin = top;\n if (top.endScope && top.endScope._wrap) {\n processBuffer();\n emitKeyword(lexeme, top.endScope._wrap);\n } else if (top.endScope && top.endScope._multi) {\n processBuffer();\n emitMultiClass(top.endScope, match);\n } else if (origin.skip) {\n modeBuffer += lexeme;\n } else {\n if (!(origin.returnEnd || origin.excludeEnd)) {\n modeBuffer += lexeme;\n }\n processBuffer();\n if (origin.excludeEnd) {\n modeBuffer = lexeme;\n }\n }\n do {\n if (top.scope) {\n emitter.closeNode();\n }\n if (!top.skip && !top.subLanguage) {\n relevance += top.relevance;\n }\n top = top.parent;\n } while (top !== endMode.parent);\n if (endMode.starts) {\n startNewMode(endMode.starts, match);\n }\n return origin.returnEnd ? 0 : lexeme.length;\n }\n\n function processContinuations() {\n const list = [];\n for (let current = top; current !== language; current = current.parent) {\n if (current.scope) {\n list.unshift(current.scope);\n }\n }\n list.forEach(item => emitter.openNode(item));\n }\n\n /** @type {{type?: MatchType, index?: number, rule?: Mode}}} */\n let lastMatch = {};\n\n /**\n * Process an individual match\n *\n * @param {string} textBeforeMatch - text preceding the match (since the last match)\n * @param {EnhancedMatch} [match] - the match itself\n */\n function processLexeme(textBeforeMatch, match) {\n const lexeme = match && match[0];\n\n // add non-matched text to the current mode buffer\n modeBuffer += textBeforeMatch;\n\n if (lexeme == null) {\n processBuffer();\n return 0;\n }\n\n // we've found a 0 width match and we're stuck, so we need to advance\n // this happens when we have badly behaved rules that have optional matchers to the degree that\n // sometimes they can end up matching nothing at all\n // Ref: https://github.com/highlightjs/highlight.js/issues/2140\n if (lastMatch.type === \"begin\" && match.type === \"end\" && lastMatch.index === match.index && lexeme === \"\") {\n // spit the \"skipped\" character that our regex choked on back into the output sequence\n modeBuffer += codeToHighlight.slice(match.index, match.index + 1);\n if (!SAFE_MODE) {\n /** @type {AnnotatedError} */\n const err = new Error(`0 width match regex (${languageName})`);\n err.languageName = languageName;\n err.badRule = lastMatch.rule;\n throw err;\n }\n return 1;\n }\n lastMatch = match;\n\n if (match.type === \"begin\") {\n return doBeginMatch(match);\n } else if (match.type === \"illegal\" && !ignoreIllegals) {\n // illegal match, we do not continue processing\n /** @type {AnnotatedError} */\n const err = new Error('Illegal lexeme \"' + lexeme + '\" for mode \"' + (top.scope || '') + '\"');\n err.mode = top;\n throw err;\n } else if (match.type === \"end\") {\n const processed = doEndMatch(match);\n if (processed !== NO_MATCH) {\n return processed;\n }\n }\n\n // edge case for when illegal matches $ (end of line) which is technically\n // a 0 width match but not a begin/end match so it's not caught by the\n // first handler (when ignoreIllegals is true)\n if (match.type === \"illegal\" && lexeme === \"\") {\n // advance so we aren't stuck in an infinite loop\n modeBuffer += \"\\n\";\n return 1;\n }\n\n // infinite loops are BAD, this is a last ditch catch all. if we have a\n // decent number of iterations yet our index (cursor position in our\n // parsing) still 3x behind our index then something is very wrong\n // so we bail\n if (iterations > 100000 && iterations > match.index * 3) {\n const err = new Error('potential infinite loop, way more iterations than matches');\n throw err;\n }\n\n /*\n Why might be find ourselves here? An potential end match that was\n triggered but could not be completed. IE, `doEndMatch` returned NO_MATCH.\n (this could be because a callback requests the match be ignored, etc)\n\n This causes no real harm other than stopping a few times too many.\n */\n\n modeBuffer += lexeme;\n return lexeme.length;\n }\n\n const language = getLanguage(languageName);\n if (!language) {\n error(LANGUAGE_NOT_FOUND.replace(\"{}\", languageName));\n throw new Error('Unknown language: \"' + languageName + '\"');\n }\n\n const md = compileLanguage(language);\n let result = '';\n /** @type {CompiledMode} */\n let top = continuation || md;\n /** @type Record */\n const continuations = {}; // keep continuations for sub-languages\n const emitter = new options.__emitter(options);\n processContinuations();\n let modeBuffer = '';\n let relevance = 0;\n let index = 0;\n let iterations = 0;\n let resumeScanAtSamePosition = false;\n\n try {\n if (!language.__emitTokens) {\n top.matcher.considerAll();\n\n for (;;) {\n iterations++;\n if (resumeScanAtSamePosition) {\n // only regexes not matched previously will now be\n // considered for a potential match\n resumeScanAtSamePosition = false;\n } else {\n top.matcher.considerAll();\n }\n top.matcher.lastIndex = index;\n\n const match = top.matcher.exec(codeToHighlight);\n // console.log(\"match\", match[0], match.rule && match.rule.begin)\n\n if (!match) break;\n\n const beforeMatch = codeToHighlight.substring(index, match.index);\n const processedCount = processLexeme(beforeMatch, match);\n index = match.index + processedCount;\n }\n processLexeme(codeToHighlight.substring(index));\n } else {\n language.__emitTokens(codeToHighlight, emitter);\n }\n\n emitter.finalize();\n result = emitter.toHTML();\n\n return {\n language: languageName,\n value: result,\n relevance,\n illegal: false,\n _emitter: emitter,\n _top: top\n };\n } catch (err) {\n if (err.message && err.message.includes('Illegal')) {\n return {\n language: languageName,\n value: escape(codeToHighlight),\n illegal: true,\n relevance: 0,\n _illegalBy: {\n message: err.message,\n index,\n context: codeToHighlight.slice(index - 100, index + 100),\n mode: err.mode,\n resultSoFar: result\n },\n _emitter: emitter\n };\n } else if (SAFE_MODE) {\n return {\n language: languageName,\n value: escape(codeToHighlight),\n illegal: false,\n relevance: 0,\n errorRaised: err,\n _emitter: emitter,\n _top: top\n };\n } else {\n throw err;\n }\n }\n }\n\n /**\n * returns a valid highlight result, without actually doing any actual work,\n * auto highlight starts with this and it's possible for small snippets that\n * auto-detection may not find a better match\n * @param {string} code\n * @returns {HighlightResult}\n */\n function justTextHighlightResult(code) {\n const result = {\n value: escape(code),\n illegal: false,\n relevance: 0,\n _top: PLAINTEXT_LANGUAGE,\n _emitter: new options.__emitter(options)\n };\n result._emitter.addText(code);\n return result;\n }\n\n /**\n Highlighting with language detection. Accepts a string with the code to\n highlight. Returns an object with the following properties:\n\n - language (detected language)\n - relevance (int)\n - value (an HTML string with highlighting markup)\n - secondBest (object with the same structure for second-best heuristically\n detected language, may be absent)\n\n @param {string} code\n @param {Array} [languageSubset]\n @returns {AutoHighlightResult}\n */\n function highlightAuto(code, languageSubset) {\n languageSubset = languageSubset || options.languages || Object.keys(languages);\n const plaintext = justTextHighlightResult(code);\n\n const results = languageSubset.filter(getLanguage).filter(autoDetection).map(name =>\n _highlight(name, code, false)\n );\n results.unshift(plaintext); // plaintext is always an option\n\n const sorted = results.sort((a, b) => {\n // sort base on relevance\n if (a.relevance !== b.relevance) return b.relevance - a.relevance;\n\n // always award the tie to the base language\n // ie if C++ and Arduino are tied, it's more likely to be C++\n if (a.language && b.language) {\n if (getLanguage(a.language).supersetOf === b.language) {\n return 1;\n } else if (getLanguage(b.language).supersetOf === a.language) {\n return -1;\n }\n }\n\n // otherwise say they are equal, which has the effect of sorting on\n // relevance while preserving the original ordering - which is how ties\n // have historically been settled, ie the language that comes first always\n // wins in the case of a tie\n return 0;\n });\n\n const [best, secondBest] = sorted;\n\n /** @type {AutoHighlightResult} */\n const result = best;\n result.secondBest = secondBest;\n\n return result;\n }\n\n /**\n * Builds new class name for block given the language name\n *\n * @param {HTMLElement} element\n * @param {string} [currentLang]\n * @param {string} [resultLang]\n */\n function updateClassName(element, currentLang, resultLang) {\n const language = (currentLang && aliases[currentLang]) || resultLang;\n\n element.classList.add(\"hljs\");\n element.classList.add(`language-${language}`);\n }\n\n /**\n * Applies highlighting to a DOM node containing code.\n *\n * @param {HighlightedHTMLElement} element - the HTML element to highlight\n */\n function highlightElement(element) {\n /** @type HTMLElement */\n let node = null;\n const language = blockLanguage(element);\n\n if (shouldNotHighlight(language)) return;\n\n fire(\"before:highlightElement\",\n { el: element, language });\n\n if (element.dataset.highlighted) {\n console.log(\"Element previously highlighted. To highlight again, first unset `dataset.highlighted`.\", element);\n return;\n }\n\n // we should be all text, no child nodes (unescaped HTML) - this is possibly\n // an HTML injection attack - it's likely too late if this is already in\n // production (the code has likely already done its damage by the time\n // we're seeing it)... but we yell loudly about this so that hopefully it's\n // more likely to be caught in development before making it to production\n if (element.children.length > 0) {\n if (!options.ignoreUnescapedHTML) {\n console.warn(\"One of your code blocks includes unescaped HTML. This is a potentially serious security risk.\");\n console.warn(\"https://github.com/highlightjs/highlight.js/wiki/security\");\n console.warn(\"The element with unescaped HTML:\");\n console.warn(element);\n }\n if (options.throwUnescapedHTML) {\n const err = new HTMLInjectionError(\n \"One of your code blocks includes unescaped HTML.\",\n element.innerHTML\n );\n throw err;\n }\n }\n\n node = element;\n const text = node.textContent;\n const result = language ? highlight(text, { language, ignoreIllegals: true }) : highlightAuto(text);\n\n element.innerHTML = result.value;\n element.dataset.highlighted = \"yes\";\n updateClassName(element, language, result.language);\n element.result = {\n language: result.language,\n // TODO: remove with version 11.0\n re: result.relevance,\n relevance: result.relevance\n };\n if (result.secondBest) {\n element.secondBest = {\n language: result.secondBest.language,\n relevance: result.secondBest.relevance\n };\n }\n\n fire(\"after:highlightElement\", { el: element, result, text });\n }\n\n /**\n * Updates highlight.js global options with the passed options\n *\n * @param {Partial} userOptions\n */\n function configure(userOptions) {\n options = inherit(options, userOptions);\n }\n\n // TODO: remove v12, deprecated\n const initHighlighting = () => {\n highlightAll();\n deprecated(\"10.6.0\", \"initHighlighting() deprecated. Use highlightAll() now.\");\n };\n\n // TODO: remove v12, deprecated\n function initHighlightingOnLoad() {\n highlightAll();\n deprecated(\"10.6.0\", \"initHighlightingOnLoad() deprecated. Use highlightAll() now.\");\n }\n\n let wantsHighlight = false;\n\n /**\n * auto-highlights all pre>code elements on the page\n */\n function highlightAll() {\n function boot() {\n // if a highlight was requested before DOM was loaded, do now\n highlightAll();\n }\n\n // if we are called too early in the loading process\n if (document.readyState === \"loading\") {\n // make sure the event listener is only added once\n if (!wantsHighlight) {\n window.addEventListener('DOMContentLoaded', boot, false);\n }\n wantsHighlight = true;\n return;\n }\n\n const blocks = document.querySelectorAll(options.cssSelector);\n blocks.forEach(highlightElement);\n }\n\n /**\n * Register a language grammar module\n *\n * @param {string} languageName\n * @param {LanguageFn} languageDefinition\n */\n function registerLanguage(languageName, languageDefinition) {\n let lang = null;\n try {\n lang = languageDefinition(hljs);\n } catch (error$1) {\n error(\"Language definition for '{}' could not be registered.\".replace(\"{}\", languageName));\n // hard or soft error\n if (!SAFE_MODE) { throw error$1; } else { error(error$1); }\n // languages that have serious errors are replaced with essentially a\n // \"plaintext\" stand-in so that the code blocks will still get normal\n // css classes applied to them - and one bad language won't break the\n // entire highlighter\n lang = PLAINTEXT_LANGUAGE;\n }\n // give it a temporary name if it doesn't have one in the meta-data\n if (!lang.name) lang.name = languageName;\n languages[languageName] = lang;\n lang.rawDefinition = languageDefinition.bind(null, hljs);\n\n if (lang.aliases) {\n registerAliases(lang.aliases, { languageName });\n }\n }\n\n /**\n * Remove a language grammar module\n *\n * @param {string} languageName\n */\n function unregisterLanguage(languageName) {\n delete languages[languageName];\n for (const alias of Object.keys(aliases)) {\n if (aliases[alias] === languageName) {\n delete aliases[alias];\n }\n }\n }\n\n /**\n * @returns {string[]} List of language internal names\n */\n function listLanguages() {\n return Object.keys(languages);\n }\n\n /**\n * @param {string} name - name of the language to retrieve\n * @returns {Language | undefined}\n */\n function getLanguage(name) {\n name = (name || '').toLowerCase();\n return languages[name] || languages[aliases[name]];\n }\n\n /**\n *\n * @param {string|string[]} aliasList - single alias or list of aliases\n * @param {{languageName: string}} opts\n */\n function registerAliases(aliasList, { languageName }) {\n if (typeof aliasList === 'string') {\n aliasList = [aliasList];\n }\n aliasList.forEach(alias => { aliases[alias.toLowerCase()] = languageName; });\n }\n\n /**\n * Determines if a given language has auto-detection enabled\n * @param {string} name - name of the language\n */\n function autoDetection(name) {\n const lang = getLanguage(name);\n return lang && !lang.disableAutodetect;\n }\n\n /**\n * Upgrades the old highlightBlock plugins to the new\n * highlightElement API\n * @param {HLJSPlugin} plugin\n */\n function upgradePluginAPI(plugin) {\n // TODO: remove with v12\n if (plugin[\"before:highlightBlock\"] && !plugin[\"before:highlightElement\"]) {\n plugin[\"before:highlightElement\"] = (data) => {\n plugin[\"before:highlightBlock\"](\n Object.assign({ block: data.el }, data)\n );\n };\n }\n if (plugin[\"after:highlightBlock\"] && !plugin[\"after:highlightElement\"]) {\n plugin[\"after:highlightElement\"] = (data) => {\n plugin[\"after:highlightBlock\"](\n Object.assign({ block: data.el }, data)\n );\n };\n }\n }\n\n /**\n * @param {HLJSPlugin} plugin\n */\n function addPlugin(plugin) {\n upgradePluginAPI(plugin);\n plugins.push(plugin);\n }\n\n /**\n * @param {HLJSPlugin} plugin\n */\n function removePlugin(plugin) {\n const index = plugins.indexOf(plugin);\n if (index !== -1) {\n plugins.splice(index, 1);\n }\n }\n\n /**\n *\n * @param {PluginEvent} event\n * @param {any} args\n */\n function fire(event, args) {\n const cb = event;\n plugins.forEach(function(plugin) {\n if (plugin[cb]) {\n plugin[cb](args);\n }\n });\n }\n\n /**\n * DEPRECATED\n * @param {HighlightedHTMLElement} el\n */\n function deprecateHighlightBlock(el) {\n deprecated(\"10.7.0\", \"highlightBlock will be removed entirely in v12.0\");\n deprecated(\"10.7.0\", \"Please use highlightElement now.\");\n\n return highlightElement(el);\n }\n\n /* Interface definition */\n Object.assign(hljs, {\n highlight,\n highlightAuto,\n highlightAll,\n highlightElement,\n // TODO: Remove with v12 API\n highlightBlock: deprecateHighlightBlock,\n configure,\n initHighlighting,\n initHighlightingOnLoad,\n registerLanguage,\n unregisterLanguage,\n listLanguages,\n getLanguage,\n registerAliases,\n autoDetection,\n inherit,\n addPlugin,\n removePlugin\n });\n\n hljs.debugMode = function() { SAFE_MODE = false; };\n hljs.safeMode = function() { SAFE_MODE = true; };\n hljs.versionString = version;\n\n hljs.regex = {\n concat: concat,\n lookahead: lookahead,\n either: either,\n optional: optional,\n anyNumberOfTimes: anyNumberOfTimes\n };\n\n for (const key in MODES) {\n // @ts-ignore\n if (typeof MODES[key] === \"object\") {\n // @ts-ignore\n deepFreeze(MODES[key]);\n }\n }\n\n // merge all the modes/regexes into our main object\n Object.assign(hljs, MODES);\n\n return hljs;\n};\n\n// Other names for the variable may break build script\nconst highlight = HLJS({});\n\n// returns a new instance of the highlighter to be used for extensions\n// check https://github.com/wooorm/lowlight/issues/47\nhighlight.newInstance = () => HLJS({});\n\nmodule.exports = highlight;\nhighlight.HighlightJS = highlight;\nhighlight.default = highlight;\n"; - var hljs = loadCommonJS(coreSrc); - var lang_yaml = loadCommonJS("/*\nLanguage: YAML\nDescription: Yet Another Markdown Language\nAuthor: Stefan Wienert \nContributors: Carl Baxter \nRequires: ruby.js\nWebsite: https://yaml.org\nCategory: common, config\n*/\nfunction yaml(hljs) {\n const LITERALS = 'true false yes no null';\n\n // YAML spec allows non-reserved URI characters in tags.\n const URI_CHARACTERS = '[\\\\w#;/?:@&=+$,.~*\\'()[\\\\]]+';\n\n // Define keys as starting with a word character\n // ...containing word chars, spaces, colons, forward-slashes, hyphens and periods\n // ...and ending with a colon followed immediately by a space, tab or newline.\n // The YAML spec allows for much more than this, but this covers most use-cases.\n const KEY = {\n className: 'attr',\n variants: [\n // added brackets support and special char support\n { begin: /[\\w*@][\\w*@ :()\\./-]*:(?=[ \\t]|$)/ },\n { // double quoted keys - with brackets and special char support\n begin: /\"[\\w*@][\\w*@ :()\\./-]*\":(?=[ \\t]|$)/ },\n { // single quoted keys - with brackets and special char support\n begin: /'[\\w*@][\\w*@ :()\\./-]*':(?=[ \\t]|$)/ },\n ]\n };\n \n const TEMPLATE_VARIABLES = {\n className: 'template-variable',\n variants: [\n { // jinja templates Ansible\n begin: /\\{\\{/,\n end: /\\}\\}/\n },\n { // Ruby i18n\n begin: /%\\{/,\n end: /\\}/\n }\n ]\n };\n\n const SINGLE_QUOTE_STRING = {\n className: 'string',\n relevance: 0,\n begin: /'/,\n end: /'/,\n contains: [\n {\n match: /''/,\n scope: 'char.escape',\n relevance: 0\n }\n ]\n };\n\n const STRING = {\n className: 'string',\n relevance: 0,\n variants: [\n {\n begin: /\"/,\n end: /\"/\n },\n { begin: /\\S+/ }\n ],\n contains: [\n hljs.BACKSLASH_ESCAPE,\n TEMPLATE_VARIABLES\n ]\n };\n\n // Strings inside of value containers (objects) can't contain braces,\n // brackets, or commas\n const CONTAINER_STRING = hljs.inherit(STRING, { variants: [\n {\n begin: /'/,\n end: /'/,\n contains: [\n {\n begin: /''/,\n relevance: 0\n }\n ]\n },\n {\n begin: /\"/,\n end: /\"/\n },\n { begin: /[^\\s,{}[\\]]+/ }\n ] });\n\n const DATE_RE = '[0-9]{4}(-[0-9][0-9]){0,2}';\n const TIME_RE = '([Tt \\\\t][0-9][0-9]?(:[0-9][0-9]){2})?';\n const FRACTION_RE = '(\\\\.[0-9]*)?';\n const ZONE_RE = '([ \\\\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?';\n const TIMESTAMP = {\n className: 'number',\n begin: '\\\\b' + DATE_RE + TIME_RE + FRACTION_RE + ZONE_RE + '\\\\b'\n };\n\n const VALUE_CONTAINER = {\n end: ',',\n endsWithParent: true,\n excludeEnd: true,\n keywords: LITERALS,\n relevance: 0\n };\n const OBJECT = {\n begin: /\\{/,\n end: /\\}/,\n contains: [ VALUE_CONTAINER ],\n illegal: '\\\\n',\n relevance: 0\n };\n const ARRAY = {\n begin: '\\\\[',\n end: '\\\\]',\n contains: [ VALUE_CONTAINER ],\n illegal: '\\\\n',\n relevance: 0\n };\n\n const MODES = [\n KEY,\n {\n className: 'meta',\n begin: '^---\\\\s*$',\n relevance: 10\n },\n { // multi line string\n // Blocks start with a | or > followed by a newline\n //\n // Indentation of subsequent lines must be the same to\n // be considered part of the block\n className: 'string',\n begin: '[\\\\|>]([1-9]?[+-])?[ ]*\\\\n( +)[^ ][^\\\\n]*\\\\n(\\\\2[^\\\\n]+\\\\n?)*'\n },\n { // Ruby/Rails erb\n begin: '<%[%=-]?',\n end: '[%-]?%>',\n subLanguage: 'ruby',\n excludeBegin: true,\n excludeEnd: true,\n relevance: 0\n },\n { // named tags\n className: 'type',\n begin: '!\\\\w+!' + URI_CHARACTERS\n },\n // https://yaml.org/spec/1.2/spec.html#id2784064\n { // verbatim tags\n className: 'type',\n begin: '!<' + URI_CHARACTERS + \">\"\n },\n { // primary tags\n className: 'type',\n begin: '!' + URI_CHARACTERS\n },\n { // secondary tags\n className: 'type',\n begin: '!!' + URI_CHARACTERS\n },\n { // fragment id &ref\n className: 'meta',\n begin: '&' + hljs.UNDERSCORE_IDENT_RE + '$'\n },\n { // fragment reference *ref\n className: 'meta',\n begin: '\\\\*' + hljs.UNDERSCORE_IDENT_RE + '$'\n },\n { // array listing\n className: 'bullet',\n // TODO: remove |$ hack when we have proper look-ahead support\n begin: '-(?=[ ]|$)',\n relevance: 0\n },\n hljs.HASH_COMMENT_MODE,\n {\n beginKeywords: LITERALS,\n keywords: { literal: LITERALS }\n },\n TIMESTAMP,\n // numbers are any valid C-style number that\n // sit isolated from other words\n {\n className: 'number',\n begin: hljs.C_NUMBER_RE + '\\\\b',\n relevance: 0\n },\n OBJECT,\n ARRAY,\n SINGLE_QUOTE_STRING,\n STRING\n ];\n\n const VALUE_MODES = [ ...MODES ];\n VALUE_MODES.pop();\n VALUE_MODES.push(CONTAINER_STRING);\n VALUE_CONTAINER.contains = VALUE_MODES;\n\n return {\n name: 'YAML',\n case_insensitive: true,\n aliases: [ 'yml' ],\n contains: MODES\n };\n}\n\nmodule.exports = yaml;\n"); - hljs.registerLanguage("yaml", lang_yaml); - var lang_json = loadCommonJS("/*\nLanguage: JSON\nDescription: JSON (JavaScript Object Notation) is a lightweight data-interchange format.\nAuthor: Ivan Sagalaev \nWebsite: http://www.json.org\nCategory: common, protocols, web\n*/\n\nfunction json(hljs) {\n const ATTRIBUTE = {\n className: 'attr',\n begin: /\"(\\\\.|[^\\\\\"\\r\\n])*\"(?=\\s*:)/,\n relevance: 1.01\n };\n const PUNCTUATION = {\n match: /[{}[\\],:]/,\n className: \"punctuation\",\n relevance: 0\n };\n const LITERALS = [\n \"true\",\n \"false\",\n \"null\"\n ];\n // NOTE: normally we would rely on `keywords` for this but using a mode here allows us\n // - to use the very tight `illegal: \\S` rule later to flag any other character\n // - as illegal indicating that despite looking like JSON we do not truly have\n // - JSON and thus improve false-positively greatly since JSON will try and claim\n // - all sorts of JSON looking stuff\n const LITERALS_MODE = {\n scope: \"literal\",\n beginKeywords: LITERALS.join(\" \"),\n };\n\n return {\n name: 'JSON',\n aliases: ['jsonc'],\n keywords:{\n literal: LITERALS,\n },\n contains: [\n ATTRIBUTE,\n PUNCTUATION,\n hljs.QUOTE_STRING_MODE,\n LITERALS_MODE,\n hljs.C_NUMBER_MODE,\n hljs.C_LINE_COMMENT_MODE,\n hljs.C_BLOCK_COMMENT_MODE\n ],\n illegal: '\\\\S'\n };\n}\n\nmodule.exports = json;\n"); - hljs.registerLanguage("json", lang_json); - var lang_javascript = loadCommonJS("const IDENT_RE = '[A-Za-z$_][0-9A-Za-z$_]*';\nconst KEYWORDS = [\n \"as\", // for exports\n \"in\",\n \"of\",\n \"if\",\n \"for\",\n \"while\",\n \"finally\",\n \"var\",\n \"new\",\n \"function\",\n \"do\",\n \"return\",\n \"void\",\n \"else\",\n \"break\",\n \"catch\",\n \"instanceof\",\n \"with\",\n \"throw\",\n \"case\",\n \"default\",\n \"try\",\n \"switch\",\n \"continue\",\n \"typeof\",\n \"delete\",\n \"let\",\n \"yield\",\n \"const\",\n \"class\",\n // JS handles these with a special rule\n // \"get\",\n // \"set\",\n \"debugger\",\n \"async\",\n \"await\",\n \"static\",\n \"import\",\n \"from\",\n \"export\",\n \"extends\",\n // It's reached stage 3, which is \"recommended for implementation\":\n \"using\"\n];\nconst LITERALS = [\n \"true\",\n \"false\",\n \"null\",\n \"undefined\",\n \"NaN\",\n \"Infinity\"\n];\n\n// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects\nconst TYPES = [\n // Fundamental objects\n \"Object\",\n \"Function\",\n \"Boolean\",\n \"Symbol\",\n // numbers and dates\n \"Math\",\n \"Date\",\n \"Number\",\n \"BigInt\",\n // text\n \"String\",\n \"RegExp\",\n // Indexed collections\n \"Array\",\n \"Float32Array\",\n \"Float64Array\",\n \"Int8Array\",\n \"Uint8Array\",\n \"Uint8ClampedArray\",\n \"Int16Array\",\n \"Int32Array\",\n \"Uint16Array\",\n \"Uint32Array\",\n \"BigInt64Array\",\n \"BigUint64Array\",\n // Keyed collections\n \"Set\",\n \"Map\",\n \"WeakSet\",\n \"WeakMap\",\n // Structured data\n \"ArrayBuffer\",\n \"SharedArrayBuffer\",\n \"Atomics\",\n \"DataView\",\n \"JSON\",\n // Control abstraction objects\n \"Promise\",\n \"Generator\",\n \"GeneratorFunction\",\n \"AsyncFunction\",\n // Reflection\n \"Reflect\",\n \"Proxy\",\n // Internationalization\n \"Intl\",\n // WebAssembly\n \"WebAssembly\"\n];\n\nconst ERROR_TYPES = [\n \"Error\",\n \"EvalError\",\n \"InternalError\",\n \"RangeError\",\n \"ReferenceError\",\n \"SyntaxError\",\n \"TypeError\",\n \"URIError\"\n];\n\nconst BUILT_IN_GLOBALS = [\n \"setInterval\",\n \"setTimeout\",\n \"clearInterval\",\n \"clearTimeout\",\n\n \"require\",\n \"exports\",\n\n \"eval\",\n \"isFinite\",\n \"isNaN\",\n \"parseFloat\",\n \"parseInt\",\n \"decodeURI\",\n \"decodeURIComponent\",\n \"encodeURI\",\n \"encodeURIComponent\",\n \"escape\",\n \"unescape\"\n];\n\nconst BUILT_IN_VARIABLES = [\n \"arguments\",\n \"this\",\n \"super\",\n \"console\",\n \"window\",\n \"document\",\n \"localStorage\",\n \"sessionStorage\",\n \"module\",\n \"global\" // Node.js\n];\n\nconst BUILT_INS = [].concat(\n BUILT_IN_GLOBALS,\n TYPES,\n ERROR_TYPES\n);\n\n/*\nLanguage: JavaScript\nDescription: JavaScript (JS) is a lightweight, interpreted, or just-in-time compiled programming language with first-class functions.\nCategory: common, scripting, web\nWebsite: https://developer.mozilla.org/en-US/docs/Web/JavaScript\n*/\n\n\n/** @type LanguageFn */\nfunction javascript(hljs) {\n const regex = hljs.regex;\n /**\n * Takes a string like \" {\n const tag = \"',\n end: ''\n };\n // to avoid some special cases inside isTrulyOpeningTag\n const XML_SELF_CLOSING = /<[A-Za-z0-9\\\\._:-]+\\s*\\/>/;\n const XML_TAG = {\n begin: /<[A-Za-z0-9\\\\._:-]+/,\n end: /\\/[A-Za-z0-9\\\\._:-]+>|\\/>/,\n /**\n * @param {RegExpMatchArray} match\n * @param {CallbackResponse} response\n */\n isTrulyOpeningTag: (match, response) => {\n const afterMatchIndex = match[0].length + match.index;\n const nextChar = match.input[afterMatchIndex];\n if (\n // HTML should not include another raw `<` inside a tag\n // nested type?\n // `>`, etc.\n nextChar === \"<\" ||\n // the , gives away that this is not HTML\n // ``\n nextChar === \",\"\n ) {\n response.ignoreMatch();\n return;\n }\n\n // ``\n // Quite possibly a tag, lets look for a matching closing tag...\n if (nextChar === \">\") {\n // if we cannot find a matching closing tag, then we\n // will ignore it\n if (!hasClosingTag(match, { after: afterMatchIndex })) {\n response.ignoreMatch();\n }\n }\n\n // `` (self-closing)\n // handled by simpleSelfClosing rule\n\n let m;\n const afterMatch = match.input.substring(afterMatchIndex);\n\n // some more template typing stuff\n // (key?: string) => Modify<\n if ((m = afterMatch.match(/^\\s*=/))) {\n response.ignoreMatch();\n return;\n }\n\n // ``\n // technically this could be HTML, but it smells like a type\n // NOTE: This is ugh, but added specifically for https://github.com/highlightjs/highlight.js/issues/3276\n if ((m = afterMatch.match(/^\\s+extends\\s+/))) {\n if (m.index === 0) {\n response.ignoreMatch();\n // eslint-disable-next-line no-useless-return\n return;\n }\n }\n }\n };\n const KEYWORDS$1 = {\n $pattern: IDENT_RE,\n keyword: KEYWORDS,\n literal: LITERALS,\n built_in: BUILT_INS,\n \"variable.language\": BUILT_IN_VARIABLES\n };\n\n // https://tc39.es/ecma262/#sec-literals-numeric-literals\n const decimalDigits = '[0-9](_?[0-9])*';\n const frac = `\\\\.(${decimalDigits})`;\n // DecimalIntegerLiteral, including Annex B NonOctalDecimalIntegerLiteral\n // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals\n const decimalInteger = `0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*`;\n const NUMBER = {\n className: 'number',\n variants: [\n // DecimalLiteral\n { begin: `(\\\\b(${decimalInteger})((${frac})|\\\\.)?|(${frac}))` +\n `[eE][+-]?(${decimalDigits})\\\\b` },\n { begin: `\\\\b(${decimalInteger})\\\\b((${frac})\\\\b|\\\\.)?|(${frac})\\\\b` },\n\n // DecimalBigIntegerLiteral\n { begin: `\\\\b(0|[1-9](_?[0-9])*)n\\\\b` },\n\n // NonDecimalIntegerLiteral\n { begin: \"\\\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\\\b\" },\n { begin: \"\\\\b0[bB][0-1](_?[0-1])*n?\\\\b\" },\n { begin: \"\\\\b0[oO][0-7](_?[0-7])*n?\\\\b\" },\n\n // LegacyOctalIntegerLiteral (does not include underscore separators)\n // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals\n { begin: \"\\\\b0[0-7]+n?\\\\b\" },\n ],\n relevance: 0\n };\n\n const SUBST = {\n className: 'subst',\n begin: '\\\\$\\\\{',\n end: '\\\\}',\n keywords: KEYWORDS$1,\n contains: [] // defined later\n };\n const HTML_TEMPLATE = {\n begin: '\\.?html`',\n end: '',\n starts: {\n end: '`',\n returnEnd: false,\n contains: [\n hljs.BACKSLASH_ESCAPE,\n SUBST\n ],\n subLanguage: 'xml'\n }\n };\n const CSS_TEMPLATE = {\n begin: '\\.?css`',\n end: '',\n starts: {\n end: '`',\n returnEnd: false,\n contains: [\n hljs.BACKSLASH_ESCAPE,\n SUBST\n ],\n subLanguage: 'css'\n }\n };\n const GRAPHQL_TEMPLATE = {\n begin: '\\.?gql`',\n end: '',\n starts: {\n end: '`',\n returnEnd: false,\n contains: [\n hljs.BACKSLASH_ESCAPE,\n SUBST\n ],\n subLanguage: 'graphql'\n }\n };\n const TEMPLATE_STRING = {\n className: 'string',\n begin: '`',\n end: '`',\n contains: [\n hljs.BACKSLASH_ESCAPE,\n SUBST\n ]\n };\n const JSDOC_COMMENT = hljs.COMMENT(\n /\\/\\*\\*(?!\\/)/,\n '\\\\*/',\n {\n relevance: 0,\n contains: [\n {\n begin: '(?=@[A-Za-z]+)',\n relevance: 0,\n contains: [\n {\n className: 'doctag',\n begin: '@[A-Za-z]+'\n },\n {\n className: 'type',\n begin: '\\\\{',\n end: '\\\\}',\n excludeEnd: true,\n excludeBegin: true,\n relevance: 0\n },\n {\n className: 'variable',\n begin: IDENT_RE$1 + '(?=\\\\s*(-)|$)',\n endsParent: true,\n relevance: 0\n },\n // eat spaces (not newlines) so we can find\n // types or variables\n {\n begin: /(?=[^\\n])\\s/,\n relevance: 0\n }\n ]\n }\n ]\n }\n );\n const COMMENT = {\n className: \"comment\",\n variants: [\n JSDOC_COMMENT,\n hljs.C_BLOCK_COMMENT_MODE,\n hljs.C_LINE_COMMENT_MODE\n ]\n };\n const SUBST_INTERNALS = [\n hljs.APOS_STRING_MODE,\n hljs.QUOTE_STRING_MODE,\n HTML_TEMPLATE,\n CSS_TEMPLATE,\n GRAPHQL_TEMPLATE,\n TEMPLATE_STRING,\n // Skip numbers when they are part of a variable name\n { match: /\\$\\d+/ },\n NUMBER,\n // This is intentional:\n // See https://github.com/highlightjs/highlight.js/issues/3288\n // hljs.REGEXP_MODE\n ];\n SUBST.contains = SUBST_INTERNALS\n .concat({\n // we need to pair up {} inside our subst to prevent\n // it from ending too early by matching another }\n begin: /\\{/,\n end: /\\}/,\n keywords: KEYWORDS$1,\n contains: [\n \"self\"\n ].concat(SUBST_INTERNALS)\n });\n const SUBST_AND_COMMENTS = [].concat(COMMENT, SUBST.contains);\n const PARAMS_CONTAINS = SUBST_AND_COMMENTS.concat([\n // eat recursive parens in sub expressions\n {\n begin: /(\\s*)\\(/,\n end: /\\)/,\n keywords: KEYWORDS$1,\n contains: [\"self\"].concat(SUBST_AND_COMMENTS)\n }\n ]);\n const PARAMS = {\n className: 'params',\n // convert this to negative lookbehind in v12\n begin: /(\\s*)\\(/, // to match the parms with\n end: /\\)/,\n excludeBegin: true,\n excludeEnd: true,\n keywords: KEYWORDS$1,\n contains: PARAMS_CONTAINS\n };\n\n // ES6 classes\n const CLASS_OR_EXTENDS = {\n variants: [\n // class Car extends vehicle\n {\n match: [\n /class/,\n /\\s+/,\n IDENT_RE$1,\n /\\s+/,\n /extends/,\n /\\s+/,\n regex.concat(IDENT_RE$1, \"(\", regex.concat(/\\./, IDENT_RE$1), \")*\")\n ],\n scope: {\n 1: \"keyword\",\n 3: \"title.class\",\n 5: \"keyword\",\n 7: \"title.class.inherited\"\n }\n },\n // class Car\n {\n match: [\n /class/,\n /\\s+/,\n IDENT_RE$1\n ],\n scope: {\n 1: \"keyword\",\n 3: \"title.class\"\n }\n },\n\n ]\n };\n\n const CLASS_REFERENCE = {\n relevance: 0,\n match:\n regex.either(\n // Hard coded exceptions\n /\\bJSON/,\n // Float32Array, OutT\n /\\b[A-Z][a-z]+([A-Z][a-z]*|\\d)*/,\n // CSSFactory, CSSFactoryT\n /\\b[A-Z]{2,}([A-Z][a-z]+|\\d)+([A-Z][a-z]*)*/,\n // FPs, FPsT\n /\\b[A-Z]{2,}[a-z]+([A-Z][a-z]+|\\d)*([A-Z][a-z]*)*/,\n // P\n // single letters are not highlighted\n // BLAH\n // this will be flagged as a UPPER_CASE_CONSTANT instead\n ),\n className: \"title.class\",\n keywords: {\n _: [\n // se we still get relevance credit for JS library classes\n ...TYPES,\n ...ERROR_TYPES\n ]\n }\n };\n\n const USE_STRICT = {\n label: \"use_strict\",\n className: 'meta',\n relevance: 10,\n begin: /^\\s*['\"]use (strict|asm)['\"]/\n };\n\n const FUNCTION_DEFINITION = {\n variants: [\n {\n match: [\n /function/,\n /\\s+/,\n IDENT_RE$1,\n /(?=\\s*\\()/\n ]\n },\n // anonymous function\n {\n match: [\n /function/,\n /\\s*(?=\\()/\n ]\n }\n ],\n className: {\n 1: \"keyword\",\n 3: \"title.function\"\n },\n label: \"func.def\",\n contains: [ PARAMS ],\n illegal: /%/\n };\n\n const UPPER_CASE_CONSTANT = {\n relevance: 0,\n match: /\\b[A-Z][A-Z_0-9]+\\b/,\n className: \"variable.constant\"\n };\n\n function noneOf(list) {\n return regex.concat(\"(?!\", list.join(\"|\"), \")\");\n }\n\n const FUNCTION_CALL = {\n match: regex.concat(\n /\\b/,\n noneOf([\n ...BUILT_IN_GLOBALS,\n \"super\",\n \"import\"\n ].map(x => `${x}\\\\s*\\\\(`)),\n IDENT_RE$1, regex.lookahead(/\\s*\\(/)),\n className: \"title.function\",\n relevance: 0\n };\n\n const PROPERTY_ACCESS = {\n begin: regex.concat(/\\./, regex.lookahead(\n regex.concat(IDENT_RE$1, /(?![0-9A-Za-z$_(])/)\n )),\n end: IDENT_RE$1,\n excludeBegin: true,\n keywords: \"prototype\",\n className: \"property\",\n relevance: 0\n };\n\n const GETTER_OR_SETTER = {\n match: [\n /get|set/,\n /\\s+/,\n IDENT_RE$1,\n /(?=\\()/\n ],\n className: {\n 1: \"keyword\",\n 3: \"title.function\"\n },\n contains: [\n { // eat to avoid empty params\n begin: /\\(\\)/\n },\n PARAMS\n ]\n };\n\n const FUNC_LEAD_IN_RE = '(\\\\(' +\n '[^()]*(\\\\(' +\n '[^()]*(\\\\(' +\n '[^()]*' +\n '\\\\)[^()]*)*' +\n '\\\\)[^()]*)*' +\n '\\\\)|' + hljs.UNDERSCORE_IDENT_RE + ')\\\\s*=>';\n\n const FUNCTION_VARIABLE = {\n match: [\n /const|var|let/, /\\s+/,\n IDENT_RE$1, /\\s*/,\n /=\\s*/,\n /(async\\s*)?/, // async is optional\n regex.lookahead(FUNC_LEAD_IN_RE)\n ],\n keywords: \"async\",\n className: {\n 1: \"keyword\",\n 3: \"title.function\"\n },\n contains: [\n PARAMS\n ]\n };\n\n return {\n name: 'JavaScript',\n aliases: ['js', 'jsx', 'mjs', 'cjs'],\n keywords: KEYWORDS$1,\n // this will be extended by TypeScript\n exports: { PARAMS_CONTAINS, CLASS_REFERENCE },\n illegal: /#(?![$_A-z])/,\n contains: [\n hljs.SHEBANG({\n label: \"shebang\",\n binary: \"node\",\n relevance: 5\n }),\n USE_STRICT,\n hljs.APOS_STRING_MODE,\n hljs.QUOTE_STRING_MODE,\n HTML_TEMPLATE,\n CSS_TEMPLATE,\n GRAPHQL_TEMPLATE,\n TEMPLATE_STRING,\n COMMENT,\n // Skip numbers when they are part of a variable name\n { match: /\\$\\d+/ },\n NUMBER,\n CLASS_REFERENCE,\n {\n scope: 'attr',\n match: IDENT_RE$1 + regex.lookahead(':'),\n relevance: 0\n },\n FUNCTION_VARIABLE,\n { // \"value\" container\n begin: '(' + hljs.RE_STARTERS_RE + '|\\\\b(case|return|throw)\\\\b)\\\\s*',\n keywords: 'return throw case',\n relevance: 0,\n contains: [\n COMMENT,\n hljs.REGEXP_MODE,\n {\n className: 'function',\n // we have to count the parens to make sure we actually have the\n // correct bounding ( ) before the =>. There could be any number of\n // sub-expressions inside also surrounded by parens.\n begin: FUNC_LEAD_IN_RE,\n returnBegin: true,\n end: '\\\\s*=>',\n contains: [\n {\n className: 'params',\n variants: [\n {\n begin: hljs.UNDERSCORE_IDENT_RE,\n relevance: 0\n },\n {\n className: null,\n begin: /\\(\\s*\\)/,\n skip: true\n },\n {\n begin: /(\\s*)\\(/,\n end: /\\)/,\n excludeBegin: true,\n excludeEnd: true,\n keywords: KEYWORDS$1,\n contains: PARAMS_CONTAINS\n }\n ]\n }\n ]\n },\n { // could be a comma delimited list of params to a function call\n begin: /,/,\n relevance: 0\n },\n {\n match: /\\s+/,\n relevance: 0\n },\n { // JSX\n variants: [\n { begin: FRAGMENT.begin, end: FRAGMENT.end },\n { match: XML_SELF_CLOSING },\n {\n begin: XML_TAG.begin,\n // we carefully check the opening tag to see if it truly\n // is a tag and not a false positive\n 'on:begin': XML_TAG.isTrulyOpeningTag,\n end: XML_TAG.end\n }\n ],\n subLanguage: 'xml',\n contains: [\n {\n begin: XML_TAG.begin,\n end: XML_TAG.end,\n skip: true,\n contains: ['self']\n }\n ]\n }\n ],\n },\n FUNCTION_DEFINITION,\n {\n // prevent this from getting swallowed up by function\n // since they appear \"function like\"\n beginKeywords: \"while if switch catch for\"\n },\n {\n // we have to count the parens to make sure we actually have the correct\n // bounding ( ). There could be any number of sub-expressions inside\n // also surrounded by parens.\n begin: '\\\\b(?!function)' + hljs.UNDERSCORE_IDENT_RE +\n '\\\\(' + // first parens\n '[^()]*(\\\\(' +\n '[^()]*(\\\\(' +\n '[^()]*' +\n '\\\\)[^()]*)*' +\n '\\\\)[^()]*)*' +\n '\\\\)\\\\s*\\\\{', // end parens\n returnBegin:true,\n label: \"func.def\",\n contains: [\n PARAMS,\n hljs.inherit(hljs.TITLE_MODE, { begin: IDENT_RE$1, className: \"title.function\" })\n ]\n },\n // catch ... so it won't trigger the property rule below\n {\n match: /\\.\\.\\./,\n relevance: 0\n },\n PROPERTY_ACCESS,\n // hack: prevents detection of keywords in some circumstances\n // .keyword()\n // $keyword = x\n {\n match: '\\\\$' + IDENT_RE$1,\n relevance: 0\n },\n {\n match: [ /\\bconstructor(?=\\s*\\()/ ],\n className: { 1: \"title.function\" },\n contains: [ PARAMS ]\n },\n FUNCTION_CALL,\n UPPER_CASE_CONSTANT,\n CLASS_OR_EXTENDS,\n GETTER_OR_SETTER,\n {\n match: /\\$[(.]/ // relevance booster for a pattern common to JS libs: `$(something)` and `$.something`\n }\n ]\n };\n}\n\nmodule.exports = javascript;\n"); - hljs.registerLanguage("javascript", lang_javascript); - var lang_bash = loadCommonJS("/*\nLanguage: Bash\nAuthor: vah \nContributrors: Benjamin Pannell \nWebsite: https://www.gnu.org/software/bash/\nCategory: common, scripting\n*/\n\n/** @type LanguageFn */\nfunction bash(hljs) {\n const regex = hljs.regex;\n const VAR = {};\n const BRACED_VAR = {\n begin: /\\$\\{/,\n end: /\\}/,\n contains: [\n \"self\",\n {\n begin: /:-/,\n contains: [ VAR ]\n } // default values\n ]\n };\n Object.assign(VAR, {\n className: 'variable',\n variants: [\n { begin: regex.concat(/\\$[\\w\\d#@][\\w\\d_]*/,\n // negative look-ahead tries to avoid matching patterns that are not\n // Perl at all like $ident$, @ident@, etc.\n `(?![\\\\w\\\\d])(?![$])`) },\n BRACED_VAR\n ]\n });\n\n const SUBST = {\n className: 'subst',\n begin: /\\$\\(/,\n end: /\\)/,\n contains: [ hljs.BACKSLASH_ESCAPE ]\n };\n const COMMENT = hljs.inherit(\n hljs.COMMENT(),\n {\n match: [\n /(^|\\s)/,\n /#.*$/\n ],\n scope: {\n 2: 'comment'\n }\n }\n );\n const HERE_DOC = {\n begin: /<<-?\\s*(?=\\w+)/,\n starts: { contains: [\n hljs.END_SAME_AS_BEGIN({\n begin: /(\\w+)/,\n end: /(\\w+)/,\n className: 'string'\n })\n ] }\n };\n const QUOTE_STRING = {\n className: 'string',\n begin: /\"/,\n end: /\"/,\n contains: [\n hljs.BACKSLASH_ESCAPE,\n VAR,\n SUBST\n ]\n };\n SUBST.contains.push(QUOTE_STRING);\n const ESCAPED_QUOTE = {\n match: /\\\\\"/\n };\n const APOS_STRING = {\n className: 'string',\n begin: /'/,\n end: /'/\n };\n const ESCAPED_APOS = {\n match: /\\\\'/\n };\n const ARITHMETIC = {\n begin: /\\$?\\(\\(/,\n end: /\\)\\)/,\n contains: [\n {\n begin: /\\d+#[0-9a-f]+/,\n className: \"number\"\n },\n hljs.NUMBER_MODE,\n VAR\n ]\n };\n const SH_LIKE_SHELLS = [\n \"fish\",\n \"bash\",\n \"zsh\",\n \"sh\",\n \"csh\",\n \"ksh\",\n \"tcsh\",\n \"dash\",\n \"scsh\",\n ];\n const KNOWN_SHEBANG = hljs.SHEBANG({\n binary: `(${SH_LIKE_SHELLS.join(\"|\")})`,\n relevance: 10\n });\n const FUNCTION = {\n className: 'function',\n begin: /\\w[\\w\\d_]*\\s*\\(\\s*\\)\\s*\\{/,\n returnBegin: true,\n contains: [ hljs.inherit(hljs.TITLE_MODE, { begin: /\\w[\\w\\d_]*/ }) ],\n relevance: 0\n };\n\n const KEYWORDS = [\n \"if\",\n \"then\",\n \"else\",\n \"elif\",\n \"fi\",\n \"time\",\n \"for\",\n \"while\",\n \"until\",\n \"in\",\n \"do\",\n \"done\",\n \"case\",\n \"esac\",\n \"coproc\",\n \"function\",\n \"select\"\n ];\n\n const LITERALS = [\n \"true\",\n \"false\"\n ];\n\n // to consume paths to prevent keyword matches inside them\n const PATH_MODE = { match: /(\\/[a-z._-]+)+/ };\n\n // http://www.gnu.org/software/bash/manual/html_node/Shell-Builtin-Commands.html\n const SHELL_BUILT_INS = [\n \"break\",\n \"cd\",\n \"continue\",\n \"eval\",\n \"exec\",\n \"exit\",\n \"export\",\n \"getopts\",\n \"hash\",\n \"pwd\",\n \"readonly\",\n \"return\",\n \"shift\",\n \"test\",\n \"times\",\n \"trap\",\n \"umask\",\n \"unset\"\n ];\n\n const BASH_BUILT_INS = [\n \"alias\",\n \"bind\",\n \"builtin\",\n \"caller\",\n \"command\",\n \"declare\",\n \"echo\",\n \"enable\",\n \"help\",\n \"let\",\n \"local\",\n \"logout\",\n \"mapfile\",\n \"printf\",\n \"read\",\n \"readarray\",\n \"source\",\n \"sudo\",\n \"type\",\n \"typeset\",\n \"ulimit\",\n \"unalias\"\n ];\n\n const ZSH_BUILT_INS = [\n \"autoload\",\n \"bg\",\n \"bindkey\",\n \"bye\",\n \"cap\",\n \"chdir\",\n \"clone\",\n \"comparguments\",\n \"compcall\",\n \"compctl\",\n \"compdescribe\",\n \"compfiles\",\n \"compgroups\",\n \"compquote\",\n \"comptags\",\n \"comptry\",\n \"compvalues\",\n \"dirs\",\n \"disable\",\n \"disown\",\n \"echotc\",\n \"echoti\",\n \"emulate\",\n \"fc\",\n \"fg\",\n \"float\",\n \"functions\",\n \"getcap\",\n \"getln\",\n \"history\",\n \"integer\",\n \"jobs\",\n \"kill\",\n \"limit\",\n \"log\",\n \"noglob\",\n \"popd\",\n \"print\",\n \"pushd\",\n \"pushln\",\n \"rehash\",\n \"sched\",\n \"setcap\",\n \"setopt\",\n \"stat\",\n \"suspend\",\n \"ttyctl\",\n \"unfunction\",\n \"unhash\",\n \"unlimit\",\n \"unsetopt\",\n \"vared\",\n \"wait\",\n \"whence\",\n \"where\",\n \"which\",\n \"zcompile\",\n \"zformat\",\n \"zftp\",\n \"zle\",\n \"zmodload\",\n \"zparseopts\",\n \"zprof\",\n \"zpty\",\n \"zregexparse\",\n \"zsocket\",\n \"zstyle\",\n \"ztcp\"\n ];\n\n const GNU_CORE_UTILS = [\n \"chcon\",\n \"chgrp\",\n \"chown\",\n \"chmod\",\n \"cp\",\n \"dd\",\n \"df\",\n \"dir\",\n \"dircolors\",\n \"ln\",\n \"ls\",\n \"mkdir\",\n \"mkfifo\",\n \"mknod\",\n \"mktemp\",\n \"mv\",\n \"realpath\",\n \"rm\",\n \"rmdir\",\n \"shred\",\n \"sync\",\n \"touch\",\n \"truncate\",\n \"vdir\",\n \"b2sum\",\n \"base32\",\n \"base64\",\n \"cat\",\n \"cksum\",\n \"comm\",\n \"csplit\",\n \"cut\",\n \"expand\",\n \"fmt\",\n \"fold\",\n \"head\",\n \"join\",\n \"md5sum\",\n \"nl\",\n \"numfmt\",\n \"od\",\n \"paste\",\n \"ptx\",\n \"pr\",\n \"sha1sum\",\n \"sha224sum\",\n \"sha256sum\",\n \"sha384sum\",\n \"sha512sum\",\n \"shuf\",\n \"sort\",\n \"split\",\n \"sum\",\n \"tac\",\n \"tail\",\n \"tr\",\n \"tsort\",\n \"unexpand\",\n \"uniq\",\n \"wc\",\n \"arch\",\n \"basename\",\n \"chroot\",\n \"date\",\n \"dirname\",\n \"du\",\n \"echo\",\n \"env\",\n \"expr\",\n \"factor\",\n // \"false\", // keyword literal already\n \"groups\",\n \"hostid\",\n \"id\",\n \"link\",\n \"logname\",\n \"nice\",\n \"nohup\",\n \"nproc\",\n \"pathchk\",\n \"pinky\",\n \"printenv\",\n \"printf\",\n \"pwd\",\n \"readlink\",\n \"runcon\",\n \"seq\",\n \"sleep\",\n \"stat\",\n \"stdbuf\",\n \"stty\",\n \"tee\",\n \"test\",\n \"timeout\",\n // \"true\", // keyword literal already\n \"tty\",\n \"uname\",\n \"unlink\",\n \"uptime\",\n \"users\",\n \"who\",\n \"whoami\",\n \"yes\"\n ];\n\n return {\n name: 'Bash',\n aliases: [\n 'sh',\n 'zsh'\n ],\n keywords: {\n $pattern: /\\b[a-z][a-z0-9._-]+\\b/,\n keyword: KEYWORDS,\n literal: LITERALS,\n built_in: [\n ...SHELL_BUILT_INS,\n ...BASH_BUILT_INS,\n // Shell modifiers\n \"set\",\n \"shopt\",\n ...ZSH_BUILT_INS,\n ...GNU_CORE_UTILS\n ]\n },\n contains: [\n KNOWN_SHEBANG, // to catch known shells and boost relevancy\n hljs.SHEBANG(), // to catch unknown shells but still highlight the shebang\n FUNCTION,\n ARITHMETIC,\n COMMENT,\n HERE_DOC,\n PATH_MODE,\n QUOTE_STRING,\n ESCAPED_QUOTE,\n APOS_STRING,\n ESCAPED_APOS,\n VAR\n ]\n };\n}\n\nmodule.exports = bash;\n"); - hljs.registerLanguage("bash", lang_bash); - var lang_markdown = loadCommonJS("/*\nLanguage: Markdown\nRequires: xml.js\nAuthor: John Crepezzi \nWebsite: https://daringfireball.net/projects/markdown/\nCategory: common, markup\n*/\n\nfunction markdown(hljs) {\n const regex = hljs.regex;\n const INLINE_HTML = {\n begin: /<\\/?[A-Za-z_]/,\n end: '>',\n subLanguage: 'xml',\n relevance: 0\n };\n const HORIZONTAL_RULE = {\n begin: '^[-\\\\*]{3,}',\n end: '$'\n };\n const CODE = {\n className: 'code',\n variants: [\n // TODO: fix to allow these to work with sublanguage also\n { begin: '(`{3,})[^`](.|\\\\n)*?\\\\1`*[ ]*' },\n { begin: '(~{3,})[^~](.|\\\\n)*?\\\\1~*[ ]*' },\n // needed to allow markdown as a sublanguage to work\n {\n begin: '```',\n end: '```+[ ]*$'\n },\n {\n begin: '~~~',\n end: '~~~+[ ]*$'\n },\n { begin: '`.+?`' },\n {\n begin: '(?=^( {4}|\\\\t))',\n // use contains to gobble up multiple lines to allow the block to be whatever size\n // but only have a single open/close tag vs one per line\n contains: [\n {\n begin: '^( {4}|\\\\t)',\n end: '(\\\\n)$'\n }\n ],\n relevance: 0\n }\n ]\n };\n const LIST = {\n className: 'bullet',\n begin: '^[ \\t]*([*+-]|(\\\\d+\\\\.))(?=\\\\s+)',\n end: '\\\\s+',\n excludeEnd: true\n };\n const LINK_REFERENCE = {\n begin: /^\\[[^\\n]+\\]:/,\n returnBegin: true,\n contains: [\n {\n className: 'symbol',\n begin: /\\[/,\n end: /\\]/,\n excludeBegin: true,\n excludeEnd: true\n },\n {\n className: 'link',\n begin: /:\\s*/,\n end: /$/,\n excludeBegin: true\n }\n ]\n };\n const URL_SCHEME = /[A-Za-z][A-Za-z0-9+.-]*/;\n const LINK = {\n variants: [\n // too much like nested array access in so many languages\n // to have any real relevance\n {\n begin: /\\[.+?\\]\\[.*?\\]/,\n relevance: 0\n },\n // popular internet URLs\n {\n begin: /\\[.+?\\]\\(((data|javascript|mailto):|(?:http|ftp)s?:\\/\\/).*?\\)/,\n relevance: 2\n },\n {\n begin: regex.concat(/\\[.+?\\]\\(/, URL_SCHEME, /:\\/\\/.*?\\)/),\n relevance: 2\n },\n // relative urls\n {\n begin: /\\[.+?\\]\\([./?&#].*?\\)/,\n relevance: 1\n },\n // whatever else, lower relevance (might not be a link at all)\n {\n begin: /\\[.*?\\]\\(.*?\\)/,\n relevance: 0\n }\n ],\n returnBegin: true,\n contains: [\n {\n // empty strings for alt or link text\n match: /\\[(?=\\])/ },\n {\n className: 'string',\n relevance: 0,\n begin: '\\\\[',\n end: '\\\\]',\n excludeBegin: true,\n returnEnd: true\n },\n {\n className: 'link',\n relevance: 0,\n begin: '\\\\]\\\\(',\n end: '\\\\)',\n excludeBegin: true,\n excludeEnd: true\n },\n {\n className: 'symbol',\n relevance: 0,\n begin: '\\\\]\\\\[',\n end: '\\\\]',\n excludeBegin: true,\n excludeEnd: true\n }\n ]\n };\n const BOLD = {\n className: 'strong',\n contains: [], // defined later\n variants: [\n {\n begin: /_{2}(?!\\s)/,\n end: /_{2}/\n },\n {\n begin: /\\*{2}(?!\\s)/,\n end: /\\*{2}/\n }\n ]\n };\n const ITALIC = {\n className: 'emphasis',\n contains: [], // defined later\n variants: [\n {\n begin: /\\*(?![*\\s])/,\n end: /\\*/\n },\n {\n begin: /_(?![_\\s])/,\n end: /_/,\n relevance: 0\n }\n ]\n };\n\n // 3 level deep nesting is not allowed because it would create confusion\n // in cases like `***testing***` because where we don't know if the last\n // `***` is starting a new bold/italic or finishing the last one\n const BOLD_WITHOUT_ITALIC = hljs.inherit(BOLD, { contains: [] });\n const ITALIC_WITHOUT_BOLD = hljs.inherit(ITALIC, { contains: [] });\n BOLD.contains.push(ITALIC_WITHOUT_BOLD);\n ITALIC.contains.push(BOLD_WITHOUT_ITALIC);\n\n let CONTAINABLE = [\n INLINE_HTML,\n LINK\n ];\n\n [\n BOLD,\n ITALIC,\n BOLD_WITHOUT_ITALIC,\n ITALIC_WITHOUT_BOLD\n ].forEach(m => {\n m.contains = m.contains.concat(CONTAINABLE);\n });\n\n CONTAINABLE = CONTAINABLE.concat(BOLD, ITALIC);\n\n const HEADER = {\n className: 'section',\n variants: [\n {\n begin: '^#{1,6}',\n end: '$',\n contains: CONTAINABLE\n },\n {\n begin: '(?=^.+?\\\\n[=-]{2,}$)',\n contains: [\n { begin: '^[=-]*$' },\n {\n begin: '^',\n end: \"\\\\n\",\n contains: CONTAINABLE\n }\n ]\n }\n ]\n };\n\n const BLOCKQUOTE = {\n className: 'quote',\n begin: '^>\\\\s+',\n contains: CONTAINABLE,\n end: '$'\n };\n\n const ENTITY = {\n //https://spec.commonmark.org/0.31.2/#entity-references\n scope: 'literal',\n match: /&([a-zA-Z0-9]+|#[0-9]{1,7}|#[Xx][0-9a-fA-F]{1,6});/\n };\n\n return {\n name: 'Markdown',\n aliases: [\n 'md',\n 'mkdown',\n 'mkd'\n ],\n contains: [\n HEADER,\n INLINE_HTML,\n LIST,\n BOLD,\n ITALIC,\n BLOCKQUOTE,\n CODE,\n HORIZONTAL_RULE,\n LINK,\n LINK_REFERENCE,\n ENTITY\n ]\n };\n}\n\nmodule.exports = markdown;\n"); - hljs.registerLanguage("markdown", lang_markdown); - var lang_diff = loadCommonJS("/*\nLanguage: Diff\nDescription: Unified and context diff\nAuthor: Vasily Polovnyov \nWebsite: https://www.gnu.org/software/diffutils/\nCategory: common\n*/\n\n/** @type LanguageFn */\nfunction diff(hljs) {\n const regex = hljs.regex;\n return {\n name: 'Diff',\n aliases: [ 'patch' ],\n contains: [\n {\n className: 'meta',\n relevance: 10,\n match: regex.either(\n /^@@ +-\\d+,\\d+ +\\+\\d+,\\d+ +@@/,\n /^\\*\\*\\* +\\d+,\\d+ +\\*\\*\\*\\*$/,\n /^--- +\\d+,\\d+ +----$/\n )\n },\n {\n className: 'comment',\n variants: [\n {\n begin: regex.either(\n /Index: /,\n /^index/,\n /={3,}/,\n /^-{3}/,\n /^\\*{3} /,\n /^\\+{3}/,\n /^diff --git/\n ),\n end: /$/\n },\n { match: /^\\*{15}$/ }\n ]\n },\n {\n className: 'addition',\n begin: /^\\+/,\n end: /$/\n },\n {\n className: 'deletion',\n begin: /^-/,\n end: /$/\n },\n {\n className: 'addition',\n begin: /^!/,\n end: /$/\n }\n ]\n };\n}\n\nmodule.exports = diff;\n"); - hljs.registerLanguage("diff", lang_diff); - root.hljs = hljs; -})(typeof window !== 'undefined' ? window : globalThis); diff --git a/plugins/voyage/playground/lib/markdown-it-front-matter.min.js b/plugins/voyage/playground/lib/markdown-it-front-matter.min.js deleted file mode 100644 index 6afa393..0000000 --- a/plugins/voyage/playground/lib/markdown-it-front-matter.min.js +++ /dev/null @@ -1,152 +0,0 @@ -// vendored by scripts/vendor-playground-libs.mjs — DO NOT EDIT -// global: markdownitFrontMatter -(function (root, factory) { - var __mod = { exports: {} }; - (function (module, exports) { - // Process front matter and pass to cb - 'use strict'; - - module.exports = function front_matter_plugin(md, cb) { - var min_markers = 3, - marker_str = '-', - marker_char = marker_str.charCodeAt(0), - marker_len = marker_str.length; - - function frontMatter(state, startLine, endLine, silent) { - var pos, - nextLine, - marker_count, - token, - old_parent, - old_line_max, - start_content, - auto_closed = false, - start = state.bMarks[startLine] + state.tShift[startLine], - max = state.eMarks[startLine]; - - // Check out the first character of the first line quickly, - // this should filter out non-front matter - if (startLine !== 0 || marker_char !== state.src.charCodeAt(0)) { - return false; - } - - // Check out the rest of the marker string - // while pos <= 3 - for (pos = start + 1; pos <= max; pos++) { - if (marker_str[(pos - start) % marker_len] !== state.src[pos]) { - start_content = pos + 1; - break; - } - } - - marker_count = Math.floor((pos - start) / marker_len); - - if (marker_count < min_markers) { - return false; - } - pos -= (pos - start) % marker_len; - - // Since start is found, we can report success here in validation mode - if (silent) { - return true; - } - - // Search for the end of the block - nextLine = startLine; - - for (;;) { - nextLine++; - if (nextLine >= endLine) { - // unclosed block should be autoclosed by end of document. - // also block seems to be autoclosed by end of parent - break; - } - - if (state.src.slice(start, max) === '...') { - break; - } - - start = state.bMarks[nextLine] + state.tShift[nextLine]; - max = state.eMarks[nextLine]; - - if (start < max && state.sCount[nextLine] < state.blkIndent) { - // non-empty line with negative indent should stop the list: - // - ``` - // test - break; - } - - if (marker_char !== state.src.charCodeAt(start)) { - continue; - } - - if (state.sCount[nextLine] - state.blkIndent >= 4) { - // closing fence should be indented less than 4 spaces - continue; - } - - for (pos = start + 1; pos <= max; pos++) { - if (marker_str[(pos - start) % marker_len] !== state.src[pos]) { - break; - } - } - - // closing code fence must be at least as long as the opening one - if (Math.floor((pos - start) / marker_len) < marker_count) { - continue; - } - - // make sure tail has spaces only - pos -= (pos - start) % marker_len; - pos = state.skipSpaces(pos); - - if (pos < max) { - continue; - } - - // found! - auto_closed = true; - break; - } - - old_parent = state.parentType; - old_line_max = state.lineMax; - state.parentType = 'container'; - - // this will prevent lazy continuations from ever going past our end marker - state.lineMax = nextLine; - - token = state.push('front_matter', null, 0); - token.hidden = true; - token.markup = state.src.slice(startLine, pos); - token.block = true; - token.map = [ startLine, nextLine + (auto_closed ? 1 : 0) ]; - token.meta = state.src.slice(start_content, start - 1); - - state.parentType = old_parent; - state.lineMax = old_line_max; - state.line = nextLine + (auto_closed ? 1 : 0); - - cb(token.meta); - - return true; - } - - md.block.ruler.before( - 'table', - 'front_matter', - frontMatter, - { - alt: [ - 'paragraph', - 'reference', - 'blockquote', - 'list' - ] - } - ); - }; - - })(__mod, __mod.exports); - root["markdownitFrontMatter"] = __mod.exports; -})(typeof window !== 'undefined' ? window : globalThis); diff --git a/plugins/voyage/playground/lib/markdown-it.min.js b/plugins/voyage/playground/lib/markdown-it.min.js deleted file mode 100644 index 5e6f256..0000000 --- a/plugins/voyage/playground/lib/markdown-it.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! markdown-it 14.1.0 https://github.com/markdown-it/markdown-it @license MIT */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).markdownit=e()}(this,(function(){"use strict";const t={};function e(r,n){"string"!=typeof n&&(n=e.defaultChars);const s=function(e){let r=t[e];if(r)return r;r=t[e]=[];for(let t=0;t<128;t++){const e=String.fromCharCode(t);r.push(e)}for(let t=0;t=55296&&t<=57343?"\ufffd\ufffd\ufffd":String.fromCharCode(t),r+=6;continue}}if(240==(248&i)&&r+91114111?e+="\ufffd\ufffd\ufffd\ufffd":(t-=65536,e+=String.fromCharCode(55296+(t>>10),56320+(1023&t))),r+=9;continue}}e+="\ufffd"}}return e}))}e.defaultChars=";/?:@&=+$,#",e.componentChars="";const r={};function n(t,e,s){"string"!=typeof e&&(s=e,e=n.defaultChars),void 0===s&&(s=!0);const i=function(t){let e=r[t];if(e)return e;e=r[t]=[];for(let t=0;t<128;t++){const r=String.fromCharCode(t);/^[0-9a-z]$/i.test(r)?e.push(r):e.push("%"+("0"+t.toString(16).toUpperCase()).slice(-2))}for(let r=0;r=55296&&n<=57343){if(n>=55296&&n<=56319&&e+1=56320&&r<=57343){o+=encodeURIComponent(t[e]+t[e+1]),e++;continue}}o+="%EF%BF%BD"}else o+=encodeURIComponent(t[e])}return o}function s(t){let e="";return e+=t.protocol||"",e+=t.slashes?"//":"",e+=t.auth?t.auth+"@":"",t.hostname&&-1!==t.hostname.indexOf(":")?e+="["+t.hostname+"]":e+=t.hostname||"",e+=t.port?":"+t.port:"",e+=t.pathname||"",e+=t.search||"",e+=t.hash||"",e}function i(){this.protocol=null,this.slashes=null,this.auth=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.pathname=null}n.defaultChars=";/?:@&=+$,-_.!~*'()#",n.componentChars="-_.!~*'()";const o=/^([a-z0-9.+-]+:)/i,u=/:[0-9]*$/,c=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,a=["{","}","|","\\","^","`"].concat(["<",">",'"',"`"," ","\r","\n","\t"]),l=["'"].concat(a),h=["%","/","?",";","#"].concat(l),p=["/","?","#"],f=/^[+a-z0-9A-Z_-]{0,63}$/,d=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,_={javascript:!0,"javascript:":!0},m={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0};function g(t,e){if(t&&t instanceof i)return t;const r=new i;return r.parse(t,e),r}i.prototype.parse=function(t,e){let r,n,s,i=t;if(i=i.trim(),!e&&1===t.split("#").length){const t=c.exec(i);if(t)return this.pathname=t[1],t[2]&&(this.search=t[2]),this}let u=o.exec(i);if(u&&(u=u[0],r=u.toLowerCase(),this.protocol=u,i=i.substr(u.length)),(e||u||i.match(/^\/\/[^@\/]+@[^@\/]+/))&&(s="//"===i.substr(0,2),!s||u&&_[u]||(i=i.substr(2),this.slashes=!0)),!_[u]&&(s||u&&!m[u])){let t,e,r=-1;for(let t=0;t127?n+="x":n+=r[t];if(!n.match(f)){const n=t.slice(0,e),s=t.slice(e+1),o=r.match(d);o&&(n.push(o[1]),s.unshift(o[2])),s.length&&(i=s.join(".")+i),this.hostname=n.join(".");break}}}}this.hostname.length>255&&(this.hostname=""),o&&(this.hostname=this.hostname.substr(1,this.hostname.length-2))}const a=i.indexOf("#");-1!==a&&(this.hash=i.substr(a),i=i.slice(0,a));const l=i.indexOf("?");return-1!==l&&(this.search=i.substr(l),i=i.slice(0,l)),i&&(this.pathname=i),m[r]&&this.hostname&&!this.pathname&&(this.pathname=""),this},i.prototype.parseHost=function(t){let e=u.exec(t);e&&(e=e[0],":"!==e&&(this.port=e.substr(1)),t=t.substr(0,t.length-e.length)),t&&(this.hostname=t)};var k,D=Object.freeze({__proto__:null,decode:e,encode:n,format:s,parse:g}),C=/[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,y=/[\0-\x1F\x7F-\x9F]/,E=/[!-#%-\*,-\/:;\?@\[-\]_\{\}\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061D-\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u09FD\u0A76\u0AF0\u0C77\u0C84\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1B7D\u1B7E\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E4F\u2E52-\u2E5D\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD803[\uDEAD\uDF55-\uDF59\uDF86-\uDF89]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDC4B-\uDC4F\uDC5A\uDC5B\uDC5D\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDE60-\uDE6C\uDEB9\uDF3C-\uDF3E]|\uD806[\uDC3B\uDD44-\uDD46\uDDE2\uDE3F-\uDE46\uDE9A-\uDE9C\uDE9E-\uDEA2\uDF00-\uDF09]|\uD807[\uDC41-\uDC45\uDC70\uDC71\uDEF7\uDEF8\uDF43-\uDF4F\uDFFF]|\uD809[\uDC70-\uDC74]|\uD80B[\uDFF1\uDFF2]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD81B[\uDE97-\uDE9A\uDFE2]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]|\uD83A[\uDD5E\uDD5F]/,A=/[\$\+<->\^`\|~\xA2-\xA6\xA8\xA9\xAC\xAE-\xB1\xB4\xB8\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0384\u0385\u03F6\u0482\u058D-\u058F\u0606-\u0608\u060B\u060E\u060F\u06DE\u06E9\u06FD\u06FE\u07F6\u07FE\u07FF\u0888\u09F2\u09F3\u09FA\u09FB\u0AF1\u0B70\u0BF3-\u0BFA\u0C7F\u0D4F\u0D79\u0E3F\u0F01-\u0F03\u0F13\u0F15-\u0F17\u0F1A-\u0F1F\u0F34\u0F36\u0F38\u0FBE-\u0FC5\u0FC7-\u0FCC\u0FCE\u0FCF\u0FD5-\u0FD8\u109E\u109F\u1390-\u1399\u166D\u17DB\u1940\u19DE-\u19FF\u1B61-\u1B6A\u1B74-\u1B7C\u1FBD\u1FBF-\u1FC1\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED-\u1FEF\u1FFD\u1FFE\u2044\u2052\u207A-\u207C\u208A-\u208C\u20A0-\u20C0\u2100\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F\u218A\u218B\u2190-\u2307\u230C-\u2328\u232B-\u2426\u2440-\u244A\u249C-\u24E9\u2500-\u2767\u2794-\u27C4\u27C7-\u27E5\u27F0-\u2982\u2999-\u29D7\u29DC-\u29FB\u29FE-\u2B73\u2B76-\u2B95\u2B97-\u2BFF\u2CE5-\u2CEA\u2E50\u2E51\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFF\u3004\u3012\u3013\u3020\u3036\u3037\u303E\u303F\u309B\u309C\u3190\u3191\u3196-\u319F\u31C0-\u31E3\u31EF\u3200-\u321E\u322A-\u3247\u3250\u3260-\u327F\u328A-\u32B0\u32C0-\u33FF\u4DC0-\u4DFF\uA490-\uA4C6\uA700-\uA716\uA720\uA721\uA789\uA78A\uA828-\uA82B\uA836-\uA839\uAA77-\uAA79\uAB5B\uAB6A\uAB6B\uFB29\uFBB2-\uFBC2\uFD40-\uFD4F\uFDCF\uFDFC-\uFDFF\uFE62\uFE64-\uFE66\uFE69\uFF04\uFF0B\uFF1C-\uFF1E\uFF3E\uFF40\uFF5C\uFF5E\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFFC\uFFFD]|\uD800[\uDD37-\uDD3F\uDD79-\uDD89\uDD8C-\uDD8E\uDD90-\uDD9C\uDDA0\uDDD0-\uDDFC]|\uD802[\uDC77\uDC78\uDEC8]|\uD805\uDF3F|\uD807[\uDFD5-\uDFF1]|\uD81A[\uDF3C-\uDF3F\uDF45]|\uD82F\uDC9C|\uD833[\uDF50-\uDFC3]|\uD834[\uDC00-\uDCF5\uDD00-\uDD26\uDD29-\uDD64\uDD6A-\uDD6C\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDDEA\uDE00-\uDE41\uDE45\uDF00-\uDF56]|\uD835[\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85\uDE86]|\uD838[\uDD4F\uDEFF]|\uD83B[\uDCAC\uDCB0\uDD2E\uDEF0\uDEF1]|\uD83C[\uDC00-\uDC2B\uDC30-\uDC93\uDCA0-\uDCAE\uDCB1-\uDCBF\uDCC1-\uDCCF\uDCD1-\uDCF5\uDD0D-\uDDAD\uDDE6-\uDE02\uDE10-\uDE3B\uDE40-\uDE48\uDE50\uDE51\uDE60-\uDE65\uDF00-\uDFFF]|\uD83D[\uDC00-\uDED7\uDEDC-\uDEEC\uDEF0-\uDEFC\uDF00-\uDF76\uDF7B-\uDFD9\uDFE0-\uDFEB\uDFF0]|\uD83E[\uDC00-\uDC0B\uDC10-\uDC47\uDC50-\uDC59\uDC60-\uDC87\uDC90-\uDCAD\uDCB0\uDCB1\uDD00-\uDE53\uDE60-\uDE6D\uDE70-\uDE7C\uDE80-\uDE88\uDE90-\uDEBD\uDEBF-\uDEC5\uDECE-\uDEDB\uDEE0-\uDEE8\uDEF0-\uDEF8\uDF00-\uDF92\uDF94-\uDFCA]/,b=/[ \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]/,F=Object.freeze({__proto__:null,Any:C,Cc:y,Cf:/[\xAD\u0600-\u0605\u061C\u06DD\u070F\u0890\u0891\u08E2\u180E\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF\uFFF9-\uFFFB]|\uD804[\uDCBD\uDCCD]|\uD80D[\uDC30-\uDC3F]|\uD82F[\uDCA0-\uDCA3]|\uD834[\uDD73-\uDD7A]|\uDB40[\uDC01\uDC20-\uDC7F]/,P:E,S:A,Z:b}),x=new Uint16Array('\u1d41<\xd5\u0131\u028a\u049d\u057b\u05d0\u0675\u06de\u07a2\u07d6\u080f\u0a4a\u0a91\u0da1\u0e6d\u0f09\u0f26\u10ca\u1228\u12e1\u1415\u149d\u14c3\u14df\u1525\0\0\0\0\0\0\u156b\u16cd\u198d\u1c12\u1ddd\u1f7e\u2060\u21b0\u228d\u23c0\u23fb\u2442\u2824\u2912\u2d08\u2e48\u2fce\u3016\u32ba\u3639\u37ac\u38fe\u3a28\u3a71\u3ae0\u3b2e\u0800EMabcfglmnoprstu\\bfms\x7f\x84\x8b\x90\x95\x98\xa6\xb3\xb9\xc8\xcflig\u803b\xc6\u40c6P\u803b&\u4026cute\u803b\xc1\u40c1reve;\u4102\u0100iyx}rc\u803b\xc2\u40c2;\u4410r;\uc000\ud835\udd04rave\u803b\xc0\u40c0pha;\u4391acr;\u4100d;\u6a53\u0100gp\x9d\xa1on;\u4104f;\uc000\ud835\udd38plyFunction;\u6061ing\u803b\xc5\u40c5\u0100cs\xbe\xc3r;\uc000\ud835\udc9cign;\u6254ilde\u803b\xc3\u40c3ml\u803b\xc4\u40c4\u0400aceforsu\xe5\xfb\xfe\u0117\u011c\u0122\u0127\u012a\u0100cr\xea\xf2kslash;\u6216\u0176\xf6\xf8;\u6ae7ed;\u6306y;\u4411\u0180crt\u0105\u010b\u0114ause;\u6235noullis;\u612ca;\u4392r;\uc000\ud835\udd05pf;\uc000\ud835\udd39eve;\u42d8c\xf2\u0113mpeq;\u624e\u0700HOacdefhilorsu\u014d\u0151\u0156\u0180\u019e\u01a2\u01b5\u01b7\u01ba\u01dc\u0215\u0273\u0278\u027ecy;\u4427PY\u803b\xa9\u40a9\u0180cpy\u015d\u0162\u017aute;\u4106\u0100;i\u0167\u0168\u62d2talDifferentialD;\u6145leys;\u612d\u0200aeio\u0189\u018e\u0194\u0198ron;\u410cdil\u803b\xc7\u40c7rc;\u4108nint;\u6230ot;\u410a\u0100dn\u01a7\u01adilla;\u40b8terDot;\u40b7\xf2\u017fi;\u43a7rcle\u0200DMPT\u01c7\u01cb\u01d1\u01d6ot;\u6299inus;\u6296lus;\u6295imes;\u6297o\u0100cs\u01e2\u01f8kwiseContourIntegral;\u6232eCurly\u0100DQ\u0203\u020foubleQuote;\u601duote;\u6019\u0200lnpu\u021e\u0228\u0247\u0255on\u0100;e\u0225\u0226\u6237;\u6a74\u0180git\u022f\u0236\u023aruent;\u6261nt;\u622fourIntegral;\u622e\u0100fr\u024c\u024e;\u6102oduct;\u6210nterClockwiseContourIntegral;\u6233oss;\u6a2fcr;\uc000\ud835\udc9ep\u0100;C\u0284\u0285\u62d3ap;\u624d\u0580DJSZacefios\u02a0\u02ac\u02b0\u02b4\u02b8\u02cb\u02d7\u02e1\u02e6\u0333\u048d\u0100;o\u0179\u02a5trahd;\u6911cy;\u4402cy;\u4405cy;\u440f\u0180grs\u02bf\u02c4\u02c7ger;\u6021r;\u61a1hv;\u6ae4\u0100ay\u02d0\u02d5ron;\u410e;\u4414l\u0100;t\u02dd\u02de\u6207a;\u4394r;\uc000\ud835\udd07\u0100af\u02eb\u0327\u0100cm\u02f0\u0322ritical\u0200ADGT\u0300\u0306\u0316\u031ccute;\u40b4o\u0174\u030b\u030d;\u42d9bleAcute;\u42ddrave;\u4060ilde;\u42dcond;\u62c4ferentialD;\u6146\u0470\u033d\0\0\0\u0342\u0354\0\u0405f;\uc000\ud835\udd3b\u0180;DE\u0348\u0349\u034d\u40a8ot;\u60dcqual;\u6250ble\u0300CDLRUV\u0363\u0372\u0382\u03cf\u03e2\u03f8ontourIntegra\xec\u0239o\u0274\u0379\0\0\u037b\xbb\u0349nArrow;\u61d3\u0100eo\u0387\u03a4ft\u0180ART\u0390\u0396\u03a1rrow;\u61d0ightArrow;\u61d4e\xe5\u02cang\u0100LR\u03ab\u03c4eft\u0100AR\u03b3\u03b9rrow;\u67f8ightArrow;\u67faightArrow;\u67f9ight\u0100AT\u03d8\u03derrow;\u61d2ee;\u62a8p\u0241\u03e9\0\0\u03efrrow;\u61d1ownArrow;\u61d5erticalBar;\u6225n\u0300ABLRTa\u0412\u042a\u0430\u045e\u047f\u037crrow\u0180;BU\u041d\u041e\u0422\u6193ar;\u6913pArrow;\u61f5reve;\u4311eft\u02d2\u043a\0\u0446\0\u0450ightVector;\u6950eeVector;\u695eector\u0100;B\u0459\u045a\u61bdar;\u6956ight\u01d4\u0467\0\u0471eeVector;\u695fector\u0100;B\u047a\u047b\u61c1ar;\u6957ee\u0100;A\u0486\u0487\u62a4rrow;\u61a7\u0100ct\u0492\u0497r;\uc000\ud835\udc9frok;\u4110\u0800NTacdfglmopqstux\u04bd\u04c0\u04c4\u04cb\u04de\u04e2\u04e7\u04ee\u04f5\u0521\u052f\u0536\u0552\u055d\u0560\u0565G;\u414aH\u803b\xd0\u40d0cute\u803b\xc9\u40c9\u0180aiy\u04d2\u04d7\u04dcron;\u411arc\u803b\xca\u40ca;\u442dot;\u4116r;\uc000\ud835\udd08rave\u803b\xc8\u40c8ement;\u6208\u0100ap\u04fa\u04fecr;\u4112ty\u0253\u0506\0\0\u0512mallSquare;\u65fberySmallSquare;\u65ab\u0100gp\u0526\u052aon;\u4118f;\uc000\ud835\udd3csilon;\u4395u\u0100ai\u053c\u0549l\u0100;T\u0542\u0543\u6a75ilde;\u6242librium;\u61cc\u0100ci\u0557\u055ar;\u6130m;\u6a73a;\u4397ml\u803b\xcb\u40cb\u0100ip\u056a\u056fsts;\u6203onentialE;\u6147\u0280cfios\u0585\u0588\u058d\u05b2\u05ccy;\u4424r;\uc000\ud835\udd09lled\u0253\u0597\0\0\u05a3mallSquare;\u65fcerySmallSquare;\u65aa\u0370\u05ba\0\u05bf\0\0\u05c4f;\uc000\ud835\udd3dAll;\u6200riertrf;\u6131c\xf2\u05cb\u0600JTabcdfgorst\u05e8\u05ec\u05ef\u05fa\u0600\u0612\u0616\u061b\u061d\u0623\u066c\u0672cy;\u4403\u803b>\u403emma\u0100;d\u05f7\u05f8\u4393;\u43dcreve;\u411e\u0180eiy\u0607\u060c\u0610dil;\u4122rc;\u411c;\u4413ot;\u4120r;\uc000\ud835\udd0a;\u62d9pf;\uc000\ud835\udd3eeater\u0300EFGLST\u0635\u0644\u064e\u0656\u065b\u0666qual\u0100;L\u063e\u063f\u6265ess;\u62dbullEqual;\u6267reater;\u6aa2ess;\u6277lantEqual;\u6a7eilde;\u6273cr;\uc000\ud835\udca2;\u626b\u0400Aacfiosu\u0685\u068b\u0696\u069b\u069e\u06aa\u06be\u06caRDcy;\u442a\u0100ct\u0690\u0694ek;\u42c7;\u405eirc;\u4124r;\u610clbertSpace;\u610b\u01f0\u06af\0\u06b2f;\u610dizontalLine;\u6500\u0100ct\u06c3\u06c5\xf2\u06a9rok;\u4126mp\u0144\u06d0\u06d8ownHum\xf0\u012fqual;\u624f\u0700EJOacdfgmnostu\u06fa\u06fe\u0703\u0707\u070e\u071a\u071e\u0721\u0728\u0744\u0778\u078b\u078f\u0795cy;\u4415lig;\u4132cy;\u4401cute\u803b\xcd\u40cd\u0100iy\u0713\u0718rc\u803b\xce\u40ce;\u4418ot;\u4130r;\u6111rave\u803b\xcc\u40cc\u0180;ap\u0720\u072f\u073f\u0100cg\u0734\u0737r;\u412ainaryI;\u6148lie\xf3\u03dd\u01f4\u0749\0\u0762\u0100;e\u074d\u074e\u622c\u0100gr\u0753\u0758ral;\u622bsection;\u62c2isible\u0100CT\u076c\u0772omma;\u6063imes;\u6062\u0180gpt\u077f\u0783\u0788on;\u412ef;\uc000\ud835\udd40a;\u4399cr;\u6110ilde;\u4128\u01eb\u079a\0\u079ecy;\u4406l\u803b\xcf\u40cf\u0280cfosu\u07ac\u07b7\u07bc\u07c2\u07d0\u0100iy\u07b1\u07b5rc;\u4134;\u4419r;\uc000\ud835\udd0dpf;\uc000\ud835\udd41\u01e3\u07c7\0\u07ccr;\uc000\ud835\udca5rcy;\u4408kcy;\u4404\u0380HJacfos\u07e4\u07e8\u07ec\u07f1\u07fd\u0802\u0808cy;\u4425cy;\u440cppa;\u439a\u0100ey\u07f6\u07fbdil;\u4136;\u441ar;\uc000\ud835\udd0epf;\uc000\ud835\udd42cr;\uc000\ud835\udca6\u0580JTaceflmost\u0825\u0829\u082c\u0850\u0863\u09b3\u09b8\u09c7\u09cd\u0a37\u0a47cy;\u4409\u803b<\u403c\u0280cmnpr\u0837\u083c\u0841\u0844\u084dute;\u4139bda;\u439bg;\u67ealacetrf;\u6112r;\u619e\u0180aey\u0857\u085c\u0861ron;\u413ddil;\u413b;\u441b\u0100fs\u0868\u0970t\u0500ACDFRTUVar\u087e\u08a9\u08b1\u08e0\u08e6\u08fc\u092f\u095b\u0390\u096a\u0100nr\u0883\u088fgleBracket;\u67e8row\u0180;BR\u0899\u089a\u089e\u6190ar;\u61e4ightArrow;\u61c6eiling;\u6308o\u01f5\u08b7\0\u08c3bleBracket;\u67e6n\u01d4\u08c8\0\u08d2eeVector;\u6961ector\u0100;B\u08db\u08dc\u61c3ar;\u6959loor;\u630aight\u0100AV\u08ef\u08f5rrow;\u6194ector;\u694e\u0100er\u0901\u0917e\u0180;AV\u0909\u090a\u0910\u62a3rrow;\u61a4ector;\u695aiangle\u0180;BE\u0924\u0925\u0929\u62b2ar;\u69cfqual;\u62b4p\u0180DTV\u0937\u0942\u094cownVector;\u6951eeVector;\u6960ector\u0100;B\u0956\u0957\u61bfar;\u6958ector\u0100;B\u0965\u0966\u61bcar;\u6952ight\xe1\u039cs\u0300EFGLST\u097e\u098b\u0995\u099d\u09a2\u09adqualGreater;\u62daullEqual;\u6266reater;\u6276ess;\u6aa1lantEqual;\u6a7dilde;\u6272r;\uc000\ud835\udd0f\u0100;e\u09bd\u09be\u62d8ftarrow;\u61daidot;\u413f\u0180npw\u09d4\u0a16\u0a1bg\u0200LRlr\u09de\u09f7\u0a02\u0a10eft\u0100AR\u09e6\u09ecrrow;\u67f5ightArrow;\u67f7ightArrow;\u67f6eft\u0100ar\u03b3\u0a0aight\xe1\u03bfight\xe1\u03caf;\uc000\ud835\udd43er\u0100LR\u0a22\u0a2ceftArrow;\u6199ightArrow;\u6198\u0180cht\u0a3e\u0a40\u0a42\xf2\u084c;\u61b0rok;\u4141;\u626a\u0400acefiosu\u0a5a\u0a5d\u0a60\u0a77\u0a7c\u0a85\u0a8b\u0a8ep;\u6905y;\u441c\u0100dl\u0a65\u0a6fiumSpace;\u605flintrf;\u6133r;\uc000\ud835\udd10nusPlus;\u6213pf;\uc000\ud835\udd44c\xf2\u0a76;\u439c\u0480Jacefostu\u0aa3\u0aa7\u0aad\u0ac0\u0b14\u0b19\u0d91\u0d97\u0d9ecy;\u440acute;\u4143\u0180aey\u0ab4\u0ab9\u0aberon;\u4147dil;\u4145;\u441d\u0180gsw\u0ac7\u0af0\u0b0eative\u0180MTV\u0ad3\u0adf\u0ae8ediumSpace;\u600bhi\u0100cn\u0ae6\u0ad8\xeb\u0ad9eryThi\xee\u0ad9ted\u0100GL\u0af8\u0b06reaterGreate\xf2\u0673essLes\xf3\u0a48Line;\u400ar;\uc000\ud835\udd11\u0200Bnpt\u0b22\u0b28\u0b37\u0b3areak;\u6060BreakingSpace;\u40a0f;\u6115\u0680;CDEGHLNPRSTV\u0b55\u0b56\u0b6a\u0b7c\u0ba1\u0beb\u0c04\u0c5e\u0c84\u0ca6\u0cd8\u0d61\u0d85\u6aec\u0100ou\u0b5b\u0b64ngruent;\u6262pCap;\u626doubleVerticalBar;\u6226\u0180lqx\u0b83\u0b8a\u0b9bement;\u6209ual\u0100;T\u0b92\u0b93\u6260ilde;\uc000\u2242\u0338ists;\u6204reater\u0380;EFGLST\u0bb6\u0bb7\u0bbd\u0bc9\u0bd3\u0bd8\u0be5\u626fqual;\u6271ullEqual;\uc000\u2267\u0338reater;\uc000\u226b\u0338ess;\u6279lantEqual;\uc000\u2a7e\u0338ilde;\u6275ump\u0144\u0bf2\u0bfdownHump;\uc000\u224e\u0338qual;\uc000\u224f\u0338e\u0100fs\u0c0a\u0c27tTriangle\u0180;BE\u0c1a\u0c1b\u0c21\u62eaar;\uc000\u29cf\u0338qual;\u62ecs\u0300;EGLST\u0c35\u0c36\u0c3c\u0c44\u0c4b\u0c58\u626equal;\u6270reater;\u6278ess;\uc000\u226a\u0338lantEqual;\uc000\u2a7d\u0338ilde;\u6274ested\u0100GL\u0c68\u0c79reaterGreater;\uc000\u2aa2\u0338essLess;\uc000\u2aa1\u0338recedes\u0180;ES\u0c92\u0c93\u0c9b\u6280qual;\uc000\u2aaf\u0338lantEqual;\u62e0\u0100ei\u0cab\u0cb9verseElement;\u620cghtTriangle\u0180;BE\u0ccb\u0ccc\u0cd2\u62ebar;\uc000\u29d0\u0338qual;\u62ed\u0100qu\u0cdd\u0d0cuareSu\u0100bp\u0ce8\u0cf9set\u0100;E\u0cf0\u0cf3\uc000\u228f\u0338qual;\u62e2erset\u0100;E\u0d03\u0d06\uc000\u2290\u0338qual;\u62e3\u0180bcp\u0d13\u0d24\u0d4eset\u0100;E\u0d1b\u0d1e\uc000\u2282\u20d2qual;\u6288ceeds\u0200;EST\u0d32\u0d33\u0d3b\u0d46\u6281qual;\uc000\u2ab0\u0338lantEqual;\u62e1ilde;\uc000\u227f\u0338erset\u0100;E\u0d58\u0d5b\uc000\u2283\u20d2qual;\u6289ilde\u0200;EFT\u0d6e\u0d6f\u0d75\u0d7f\u6241qual;\u6244ullEqual;\u6247ilde;\u6249erticalBar;\u6224cr;\uc000\ud835\udca9ilde\u803b\xd1\u40d1;\u439d\u0700Eacdfgmoprstuv\u0dbd\u0dc2\u0dc9\u0dd5\u0ddb\u0de0\u0de7\u0dfc\u0e02\u0e20\u0e22\u0e32\u0e3f\u0e44lig;\u4152cute\u803b\xd3\u40d3\u0100iy\u0dce\u0dd3rc\u803b\xd4\u40d4;\u441eblac;\u4150r;\uc000\ud835\udd12rave\u803b\xd2\u40d2\u0180aei\u0dee\u0df2\u0df6cr;\u414cga;\u43a9cron;\u439fpf;\uc000\ud835\udd46enCurly\u0100DQ\u0e0e\u0e1aoubleQuote;\u601cuote;\u6018;\u6a54\u0100cl\u0e27\u0e2cr;\uc000\ud835\udcaaash\u803b\xd8\u40d8i\u016c\u0e37\u0e3cde\u803b\xd5\u40d5es;\u6a37ml\u803b\xd6\u40d6er\u0100BP\u0e4b\u0e60\u0100ar\u0e50\u0e53r;\u603eac\u0100ek\u0e5a\u0e5c;\u63deet;\u63b4arenthesis;\u63dc\u0480acfhilors\u0e7f\u0e87\u0e8a\u0e8f\u0e92\u0e94\u0e9d\u0eb0\u0efcrtialD;\u6202y;\u441fr;\uc000\ud835\udd13i;\u43a6;\u43a0usMinus;\u40b1\u0100ip\u0ea2\u0eadncareplan\xe5\u069df;\u6119\u0200;eio\u0eb9\u0eba\u0ee0\u0ee4\u6abbcedes\u0200;EST\u0ec8\u0ec9\u0ecf\u0eda\u627aqual;\u6aaflantEqual;\u627cilde;\u627eme;\u6033\u0100dp\u0ee9\u0eeeuct;\u620fortion\u0100;a\u0225\u0ef9l;\u621d\u0100ci\u0f01\u0f06r;\uc000\ud835\udcab;\u43a8\u0200Ufos\u0f11\u0f16\u0f1b\u0f1fOT\u803b"\u4022r;\uc000\ud835\udd14pf;\u611acr;\uc000\ud835\udcac\u0600BEacefhiorsu\u0f3e\u0f43\u0f47\u0f60\u0f73\u0fa7\u0faa\u0fad\u1096\u10a9\u10b4\u10bearr;\u6910G\u803b\xae\u40ae\u0180cnr\u0f4e\u0f53\u0f56ute;\u4154g;\u67ebr\u0100;t\u0f5c\u0f5d\u61a0l;\u6916\u0180aey\u0f67\u0f6c\u0f71ron;\u4158dil;\u4156;\u4420\u0100;v\u0f78\u0f79\u611cerse\u0100EU\u0f82\u0f99\u0100lq\u0f87\u0f8eement;\u620builibrium;\u61cbpEquilibrium;\u696fr\xbb\u0f79o;\u43a1ght\u0400ACDFTUVa\u0fc1\u0feb\u0ff3\u1022\u1028\u105b\u1087\u03d8\u0100nr\u0fc6\u0fd2gleBracket;\u67e9row\u0180;BL\u0fdc\u0fdd\u0fe1\u6192ar;\u61e5eftArrow;\u61c4eiling;\u6309o\u01f5\u0ff9\0\u1005bleBracket;\u67e7n\u01d4\u100a\0\u1014eeVector;\u695dector\u0100;B\u101d\u101e\u61c2ar;\u6955loor;\u630b\u0100er\u102d\u1043e\u0180;AV\u1035\u1036\u103c\u62a2rrow;\u61a6ector;\u695biangle\u0180;BE\u1050\u1051\u1055\u62b3ar;\u69d0qual;\u62b5p\u0180DTV\u1063\u106e\u1078ownVector;\u694feeVector;\u695cector\u0100;B\u1082\u1083\u61bear;\u6954ector\u0100;B\u1091\u1092\u61c0ar;\u6953\u0100pu\u109b\u109ef;\u611dndImplies;\u6970ightarrow;\u61db\u0100ch\u10b9\u10bcr;\u611b;\u61b1leDelayed;\u69f4\u0680HOacfhimoqstu\u10e4\u10f1\u10f7\u10fd\u1119\u111e\u1151\u1156\u1161\u1167\u11b5\u11bb\u11bf\u0100Cc\u10e9\u10eeHcy;\u4429y;\u4428FTcy;\u442ccute;\u415a\u0280;aeiy\u1108\u1109\u110e\u1113\u1117\u6abcron;\u4160dil;\u415erc;\u415c;\u4421r;\uc000\ud835\udd16ort\u0200DLRU\u112a\u1134\u113e\u1149ownArrow\xbb\u041eeftArrow\xbb\u089aightArrow\xbb\u0fddpArrow;\u6191gma;\u43a3allCircle;\u6218pf;\uc000\ud835\udd4a\u0272\u116d\0\0\u1170t;\u621aare\u0200;ISU\u117b\u117c\u1189\u11af\u65a1ntersection;\u6293u\u0100bp\u118f\u119eset\u0100;E\u1197\u1198\u628fqual;\u6291erset\u0100;E\u11a8\u11a9\u6290qual;\u6292nion;\u6294cr;\uc000\ud835\udcaear;\u62c6\u0200bcmp\u11c8\u11db\u1209\u120b\u0100;s\u11cd\u11ce\u62d0et\u0100;E\u11cd\u11d5qual;\u6286\u0100ch\u11e0\u1205eeds\u0200;EST\u11ed\u11ee\u11f4\u11ff\u627bqual;\u6ab0lantEqual;\u627dilde;\u627fTh\xe1\u0f8c;\u6211\u0180;es\u1212\u1213\u1223\u62d1rset\u0100;E\u121c\u121d\u6283qual;\u6287et\xbb\u1213\u0580HRSacfhiors\u123e\u1244\u1249\u1255\u125e\u1271\u1276\u129f\u12c2\u12c8\u12d1ORN\u803b\xde\u40deADE;\u6122\u0100Hc\u124e\u1252cy;\u440by;\u4426\u0100bu\u125a\u125c;\u4009;\u43a4\u0180aey\u1265\u126a\u126fron;\u4164dil;\u4162;\u4422r;\uc000\ud835\udd17\u0100ei\u127b\u1289\u01f2\u1280\0\u1287efore;\u6234a;\u4398\u0100cn\u128e\u1298kSpace;\uc000\u205f\u200aSpace;\u6009lde\u0200;EFT\u12ab\u12ac\u12b2\u12bc\u623cqual;\u6243ullEqual;\u6245ilde;\u6248pf;\uc000\ud835\udd4bipleDot;\u60db\u0100ct\u12d6\u12dbr;\uc000\ud835\udcafrok;\u4166\u0ae1\u12f7\u130e\u131a\u1326\0\u132c\u1331\0\0\0\0\0\u1338\u133d\u1377\u1385\0\u13ff\u1404\u140a\u1410\u0100cr\u12fb\u1301ute\u803b\xda\u40dar\u0100;o\u1307\u1308\u619fcir;\u6949r\u01e3\u1313\0\u1316y;\u440eve;\u416c\u0100iy\u131e\u1323rc\u803b\xdb\u40db;\u4423blac;\u4170r;\uc000\ud835\udd18rave\u803b\xd9\u40d9acr;\u416a\u0100di\u1341\u1369er\u0100BP\u1348\u135d\u0100ar\u134d\u1350r;\u405fac\u0100ek\u1357\u1359;\u63dfet;\u63b5arenthesis;\u63ddon\u0100;P\u1370\u1371\u62c3lus;\u628e\u0100gp\u137b\u137fon;\u4172f;\uc000\ud835\udd4c\u0400ADETadps\u1395\u13ae\u13b8\u13c4\u03e8\u13d2\u13d7\u13f3rrow\u0180;BD\u1150\u13a0\u13a4ar;\u6912ownArrow;\u61c5ownArrow;\u6195quilibrium;\u696eee\u0100;A\u13cb\u13cc\u62a5rrow;\u61a5own\xe1\u03f3er\u0100LR\u13de\u13e8eftArrow;\u6196ightArrow;\u6197i\u0100;l\u13f9\u13fa\u43d2on;\u43a5ing;\u416ecr;\uc000\ud835\udcb0ilde;\u4168ml\u803b\xdc\u40dc\u0480Dbcdefosv\u1427\u142c\u1430\u1433\u143e\u1485\u148a\u1490\u1496ash;\u62abar;\u6aeby;\u4412ash\u0100;l\u143b\u143c\u62a9;\u6ae6\u0100er\u1443\u1445;\u62c1\u0180bty\u144c\u1450\u147aar;\u6016\u0100;i\u144f\u1455cal\u0200BLST\u1461\u1465\u146a\u1474ar;\u6223ine;\u407ceparator;\u6758ilde;\u6240ThinSpace;\u600ar;\uc000\ud835\udd19pf;\uc000\ud835\udd4dcr;\uc000\ud835\udcb1dash;\u62aa\u0280cefos\u14a7\u14ac\u14b1\u14b6\u14bcirc;\u4174dge;\u62c0r;\uc000\ud835\udd1apf;\uc000\ud835\udd4ecr;\uc000\ud835\udcb2\u0200fios\u14cb\u14d0\u14d2\u14d8r;\uc000\ud835\udd1b;\u439epf;\uc000\ud835\udd4fcr;\uc000\ud835\udcb3\u0480AIUacfosu\u14f1\u14f5\u14f9\u14fd\u1504\u150f\u1514\u151a\u1520cy;\u442fcy;\u4407cy;\u442ecute\u803b\xdd\u40dd\u0100iy\u1509\u150drc;\u4176;\u442br;\uc000\ud835\udd1cpf;\uc000\ud835\udd50cr;\uc000\ud835\udcb4ml;\u4178\u0400Hacdefos\u1535\u1539\u153f\u154b\u154f\u155d\u1560\u1564cy;\u4416cute;\u4179\u0100ay\u1544\u1549ron;\u417d;\u4417ot;\u417b\u01f2\u1554\0\u155boWidt\xe8\u0ad9a;\u4396r;\u6128pf;\u6124cr;\uc000\ud835\udcb5\u0be1\u1583\u158a\u1590\0\u15b0\u15b6\u15bf\0\0\0\0\u15c6\u15db\u15eb\u165f\u166d\0\u1695\u169b\u16b2\u16b9\0\u16becute\u803b\xe1\u40e1reve;\u4103\u0300;Ediuy\u159c\u159d\u15a1\u15a3\u15a8\u15ad\u623e;\uc000\u223e\u0333;\u623frc\u803b\xe2\u40e2te\u80bb\xb4\u0306;\u4430lig\u803b\xe6\u40e6\u0100;r\xb2\u15ba;\uc000\ud835\udd1erave\u803b\xe0\u40e0\u0100ep\u15ca\u15d6\u0100fp\u15cf\u15d4sym;\u6135\xe8\u15d3ha;\u43b1\u0100ap\u15dfc\u0100cl\u15e4\u15e7r;\u4101g;\u6a3f\u0264\u15f0\0\0\u160a\u0280;adsv\u15fa\u15fb\u15ff\u1601\u1607\u6227nd;\u6a55;\u6a5clope;\u6a58;\u6a5a\u0380;elmrsz\u1618\u1619\u161b\u161e\u163f\u164f\u1659\u6220;\u69a4e\xbb\u1619sd\u0100;a\u1625\u1626\u6221\u0461\u1630\u1632\u1634\u1636\u1638\u163a\u163c\u163e;\u69a8;\u69a9;\u69aa;\u69ab;\u69ac;\u69ad;\u69ae;\u69aft\u0100;v\u1645\u1646\u621fb\u0100;d\u164c\u164d\u62be;\u699d\u0100pt\u1654\u1657h;\u6222\xbb\xb9arr;\u637c\u0100gp\u1663\u1667on;\u4105f;\uc000\ud835\udd52\u0380;Eaeiop\u12c1\u167b\u167d\u1682\u1684\u1687\u168a;\u6a70cir;\u6a6f;\u624ad;\u624bs;\u4027rox\u0100;e\u12c1\u1692\xf1\u1683ing\u803b\xe5\u40e5\u0180cty\u16a1\u16a6\u16a8r;\uc000\ud835\udcb6;\u402amp\u0100;e\u12c1\u16af\xf1\u0288ilde\u803b\xe3\u40e3ml\u803b\xe4\u40e4\u0100ci\u16c2\u16c8onin\xf4\u0272nt;\u6a11\u0800Nabcdefiklnoprsu\u16ed\u16f1\u1730\u173c\u1743\u1748\u1778\u177d\u17e0\u17e6\u1839\u1850\u170d\u193d\u1948\u1970ot;\u6aed\u0100cr\u16f6\u171ek\u0200ceps\u1700\u1705\u170d\u1713ong;\u624cpsilon;\u43f6rime;\u6035im\u0100;e\u171a\u171b\u623dq;\u62cd\u0176\u1722\u1726ee;\u62bded\u0100;g\u172c\u172d\u6305e\xbb\u172drk\u0100;t\u135c\u1737brk;\u63b6\u0100oy\u1701\u1741;\u4431quo;\u601e\u0280cmprt\u1753\u175b\u1761\u1764\u1768aus\u0100;e\u010a\u0109ptyv;\u69b0s\xe9\u170cno\xf5\u0113\u0180ahw\u176f\u1771\u1773;\u43b2;\u6136een;\u626cr;\uc000\ud835\udd1fg\u0380costuvw\u178d\u179d\u17b3\u17c1\u17d5\u17db\u17de\u0180aiu\u1794\u1796\u179a\xf0\u0760rc;\u65efp\xbb\u1371\u0180dpt\u17a4\u17a8\u17adot;\u6a00lus;\u6a01imes;\u6a02\u0271\u17b9\0\0\u17becup;\u6a06ar;\u6605riangle\u0100du\u17cd\u17d2own;\u65bdp;\u65b3plus;\u6a04e\xe5\u1444\xe5\u14adarow;\u690d\u0180ako\u17ed\u1826\u1835\u0100cn\u17f2\u1823k\u0180lst\u17fa\u05ab\u1802ozenge;\u69ebriangle\u0200;dlr\u1812\u1813\u1818\u181d\u65b4own;\u65beeft;\u65c2ight;\u65b8k;\u6423\u01b1\u182b\0\u1833\u01b2\u182f\0\u1831;\u6592;\u65914;\u6593ck;\u6588\u0100eo\u183e\u184d\u0100;q\u1843\u1846\uc000=\u20e5uiv;\uc000\u2261\u20e5t;\u6310\u0200ptwx\u1859\u185e\u1867\u186cf;\uc000\ud835\udd53\u0100;t\u13cb\u1863om\xbb\u13cctie;\u62c8\u0600DHUVbdhmptuv\u1885\u1896\u18aa\u18bb\u18d7\u18db\u18ec\u18ff\u1905\u190a\u1910\u1921\u0200LRlr\u188e\u1890\u1892\u1894;\u6557;\u6554;\u6556;\u6553\u0280;DUdu\u18a1\u18a2\u18a4\u18a6\u18a8\u6550;\u6566;\u6569;\u6564;\u6567\u0200LRlr\u18b3\u18b5\u18b7\u18b9;\u655d;\u655a;\u655c;\u6559\u0380;HLRhlr\u18ca\u18cb\u18cd\u18cf\u18d1\u18d3\u18d5\u6551;\u656c;\u6563;\u6560;\u656b;\u6562;\u655fox;\u69c9\u0200LRlr\u18e4\u18e6\u18e8\u18ea;\u6555;\u6552;\u6510;\u650c\u0280;DUdu\u06bd\u18f7\u18f9\u18fb\u18fd;\u6565;\u6568;\u652c;\u6534inus;\u629flus;\u629eimes;\u62a0\u0200LRlr\u1919\u191b\u191d\u191f;\u655b;\u6558;\u6518;\u6514\u0380;HLRhlr\u1930\u1931\u1933\u1935\u1937\u1939\u193b\u6502;\u656a;\u6561;\u655e;\u653c;\u6524;\u651c\u0100ev\u0123\u1942bar\u803b\xa6\u40a6\u0200ceio\u1951\u1956\u195a\u1960r;\uc000\ud835\udcb7mi;\u604fm\u0100;e\u171a\u171cl\u0180;bh\u1968\u1969\u196b\u405c;\u69c5sub;\u67c8\u016c\u1974\u197el\u0100;e\u1979\u197a\u6022t\xbb\u197ap\u0180;Ee\u012f\u1985\u1987;\u6aae\u0100;q\u06dc\u06db\u0ce1\u19a7\0\u19e8\u1a11\u1a15\u1a32\0\u1a37\u1a50\0\0\u1ab4\0\0\u1ac1\0\0\u1b21\u1b2e\u1b4d\u1b52\0\u1bfd\0\u1c0c\u0180cpr\u19ad\u19b2\u19ddute;\u4107\u0300;abcds\u19bf\u19c0\u19c4\u19ca\u19d5\u19d9\u6229nd;\u6a44rcup;\u6a49\u0100au\u19cf\u19d2p;\u6a4bp;\u6a47ot;\u6a40;\uc000\u2229\ufe00\u0100eo\u19e2\u19e5t;\u6041\xee\u0693\u0200aeiu\u19f0\u19fb\u1a01\u1a05\u01f0\u19f5\0\u19f8s;\u6a4don;\u410ddil\u803b\xe7\u40e7rc;\u4109ps\u0100;s\u1a0c\u1a0d\u6a4cm;\u6a50ot;\u410b\u0180dmn\u1a1b\u1a20\u1a26il\u80bb\xb8\u01adptyv;\u69b2t\u8100\xa2;e\u1a2d\u1a2e\u40a2r\xe4\u01b2r;\uc000\ud835\udd20\u0180cei\u1a3d\u1a40\u1a4dy;\u4447ck\u0100;m\u1a47\u1a48\u6713ark\xbb\u1a48;\u43c7r\u0380;Ecefms\u1a5f\u1a60\u1a62\u1a6b\u1aa4\u1aaa\u1aae\u65cb;\u69c3\u0180;el\u1a69\u1a6a\u1a6d\u42c6q;\u6257e\u0261\u1a74\0\0\u1a88rrow\u0100lr\u1a7c\u1a81eft;\u61baight;\u61bb\u0280RSacd\u1a92\u1a94\u1a96\u1a9a\u1a9f\xbb\u0f47;\u64c8st;\u629birc;\u629aash;\u629dnint;\u6a10id;\u6aefcir;\u69c2ubs\u0100;u\u1abb\u1abc\u6663it\xbb\u1abc\u02ec\u1ac7\u1ad4\u1afa\0\u1b0aon\u0100;e\u1acd\u1ace\u403a\u0100;q\xc7\xc6\u026d\u1ad9\0\0\u1ae2a\u0100;t\u1ade\u1adf\u402c;\u4040\u0180;fl\u1ae8\u1ae9\u1aeb\u6201\xee\u1160e\u0100mx\u1af1\u1af6ent\xbb\u1ae9e\xf3\u024d\u01e7\u1afe\0\u1b07\u0100;d\u12bb\u1b02ot;\u6a6dn\xf4\u0246\u0180fry\u1b10\u1b14\u1b17;\uc000\ud835\udd54o\xe4\u0254\u8100\xa9;s\u0155\u1b1dr;\u6117\u0100ao\u1b25\u1b29rr;\u61b5ss;\u6717\u0100cu\u1b32\u1b37r;\uc000\ud835\udcb8\u0100bp\u1b3c\u1b44\u0100;e\u1b41\u1b42\u6acf;\u6ad1\u0100;e\u1b49\u1b4a\u6ad0;\u6ad2dot;\u62ef\u0380delprvw\u1b60\u1b6c\u1b77\u1b82\u1bac\u1bd4\u1bf9arr\u0100lr\u1b68\u1b6a;\u6938;\u6935\u0270\u1b72\0\0\u1b75r;\u62dec;\u62dfarr\u0100;p\u1b7f\u1b80\u61b6;\u693d\u0300;bcdos\u1b8f\u1b90\u1b96\u1ba1\u1ba5\u1ba8\u622arcap;\u6a48\u0100au\u1b9b\u1b9ep;\u6a46p;\u6a4aot;\u628dr;\u6a45;\uc000\u222a\ufe00\u0200alrv\u1bb5\u1bbf\u1bde\u1be3rr\u0100;m\u1bbc\u1bbd\u61b7;\u693cy\u0180evw\u1bc7\u1bd4\u1bd8q\u0270\u1bce\0\0\u1bd2re\xe3\u1b73u\xe3\u1b75ee;\u62ceedge;\u62cfen\u803b\xa4\u40a4earrow\u0100lr\u1bee\u1bf3eft\xbb\u1b80ight\xbb\u1bbde\xe4\u1bdd\u0100ci\u1c01\u1c07onin\xf4\u01f7nt;\u6231lcty;\u632d\u0980AHabcdefhijlorstuwz\u1c38\u1c3b\u1c3f\u1c5d\u1c69\u1c75\u1c8a\u1c9e\u1cac\u1cb7\u1cfb\u1cff\u1d0d\u1d7b\u1d91\u1dab\u1dbb\u1dc6\u1dcdr\xf2\u0381ar;\u6965\u0200glrs\u1c48\u1c4d\u1c52\u1c54ger;\u6020eth;\u6138\xf2\u1133h\u0100;v\u1c5a\u1c5b\u6010\xbb\u090a\u016b\u1c61\u1c67arow;\u690fa\xe3\u0315\u0100ay\u1c6e\u1c73ron;\u410f;\u4434\u0180;ao\u0332\u1c7c\u1c84\u0100gr\u02bf\u1c81r;\u61catseq;\u6a77\u0180glm\u1c91\u1c94\u1c98\u803b\xb0\u40b0ta;\u43b4ptyv;\u69b1\u0100ir\u1ca3\u1ca8sht;\u697f;\uc000\ud835\udd21ar\u0100lr\u1cb3\u1cb5\xbb\u08dc\xbb\u101e\u0280aegsv\u1cc2\u0378\u1cd6\u1cdc\u1ce0m\u0180;os\u0326\u1cca\u1cd4nd\u0100;s\u0326\u1cd1uit;\u6666amma;\u43ddin;\u62f2\u0180;io\u1ce7\u1ce8\u1cf8\u40f7de\u8100\xf7;o\u1ce7\u1cf0ntimes;\u62c7n\xf8\u1cf7cy;\u4452c\u026f\u1d06\0\0\u1d0arn;\u631eop;\u630d\u0280lptuw\u1d18\u1d1d\u1d22\u1d49\u1d55lar;\u4024f;\uc000\ud835\udd55\u0280;emps\u030b\u1d2d\u1d37\u1d3d\u1d42q\u0100;d\u0352\u1d33ot;\u6251inus;\u6238lus;\u6214quare;\u62a1blebarwedg\xe5\xfan\u0180adh\u112e\u1d5d\u1d67ownarrow\xf3\u1c83arpoon\u0100lr\u1d72\u1d76ef\xf4\u1cb4igh\xf4\u1cb6\u0162\u1d7f\u1d85karo\xf7\u0f42\u026f\u1d8a\0\0\u1d8ern;\u631fop;\u630c\u0180cot\u1d98\u1da3\u1da6\u0100ry\u1d9d\u1da1;\uc000\ud835\udcb9;\u4455l;\u69f6rok;\u4111\u0100dr\u1db0\u1db4ot;\u62f1i\u0100;f\u1dba\u1816\u65bf\u0100ah\u1dc0\u1dc3r\xf2\u0429a\xf2\u0fa6angle;\u69a6\u0100ci\u1dd2\u1dd5y;\u445fgrarr;\u67ff\u0900Dacdefglmnopqrstux\u1e01\u1e09\u1e19\u1e38\u0578\u1e3c\u1e49\u1e61\u1e7e\u1ea5\u1eaf\u1ebd\u1ee1\u1f2a\u1f37\u1f44\u1f4e\u1f5a\u0100Do\u1e06\u1d34o\xf4\u1c89\u0100cs\u1e0e\u1e14ute\u803b\xe9\u40e9ter;\u6a6e\u0200aioy\u1e22\u1e27\u1e31\u1e36ron;\u411br\u0100;c\u1e2d\u1e2e\u6256\u803b\xea\u40ealon;\u6255;\u444dot;\u4117\u0100Dr\u1e41\u1e45ot;\u6252;\uc000\ud835\udd22\u0180;rs\u1e50\u1e51\u1e57\u6a9aave\u803b\xe8\u40e8\u0100;d\u1e5c\u1e5d\u6a96ot;\u6a98\u0200;ils\u1e6a\u1e6b\u1e72\u1e74\u6a99nters;\u63e7;\u6113\u0100;d\u1e79\u1e7a\u6a95ot;\u6a97\u0180aps\u1e85\u1e89\u1e97cr;\u4113ty\u0180;sv\u1e92\u1e93\u1e95\u6205et\xbb\u1e93p\u01001;\u1e9d\u1ea4\u0133\u1ea1\u1ea3;\u6004;\u6005\u6003\u0100gs\u1eaa\u1eac;\u414bp;\u6002\u0100gp\u1eb4\u1eb8on;\u4119f;\uc000\ud835\udd56\u0180als\u1ec4\u1ece\u1ed2r\u0100;s\u1eca\u1ecb\u62d5l;\u69e3us;\u6a71i\u0180;lv\u1eda\u1edb\u1edf\u43b5on\xbb\u1edb;\u43f5\u0200csuv\u1eea\u1ef3\u1f0b\u1f23\u0100io\u1eef\u1e31rc\xbb\u1e2e\u0269\u1ef9\0\0\u1efb\xed\u0548ant\u0100gl\u1f02\u1f06tr\xbb\u1e5dess\xbb\u1e7a\u0180aei\u1f12\u1f16\u1f1als;\u403dst;\u625fv\u0100;D\u0235\u1f20D;\u6a78parsl;\u69e5\u0100Da\u1f2f\u1f33ot;\u6253rr;\u6971\u0180cdi\u1f3e\u1f41\u1ef8r;\u612fo\xf4\u0352\u0100ah\u1f49\u1f4b;\u43b7\u803b\xf0\u40f0\u0100mr\u1f53\u1f57l\u803b\xeb\u40ebo;\u60ac\u0180cip\u1f61\u1f64\u1f67l;\u4021s\xf4\u056e\u0100eo\u1f6c\u1f74ctatio\xee\u0559nential\xe5\u0579\u09e1\u1f92\0\u1f9e\0\u1fa1\u1fa7\0\0\u1fc6\u1fcc\0\u1fd3\0\u1fe6\u1fea\u2000\0\u2008\u205allingdotse\xf1\u1e44y;\u4444male;\u6640\u0180ilr\u1fad\u1fb3\u1fc1lig;\u8000\ufb03\u0269\u1fb9\0\0\u1fbdg;\u8000\ufb00ig;\u8000\ufb04;\uc000\ud835\udd23lig;\u8000\ufb01lig;\uc000fj\u0180alt\u1fd9\u1fdc\u1fe1t;\u666dig;\u8000\ufb02ns;\u65b1of;\u4192\u01f0\u1fee\0\u1ff3f;\uc000\ud835\udd57\u0100ak\u05bf\u1ff7\u0100;v\u1ffc\u1ffd\u62d4;\u6ad9artint;\u6a0d\u0100ao\u200c\u2055\u0100cs\u2011\u2052\u03b1\u201a\u2030\u2038\u2045\u2048\0\u2050\u03b2\u2022\u2025\u2027\u202a\u202c\0\u202e\u803b\xbd\u40bd;\u6153\u803b\xbc\u40bc;\u6155;\u6159;\u615b\u01b3\u2034\0\u2036;\u6154;\u6156\u02b4\u203e\u2041\0\0\u2043\u803b\xbe\u40be;\u6157;\u615c5;\u6158\u01b6\u204c\0\u204e;\u615a;\u615d8;\u615el;\u6044wn;\u6322cr;\uc000\ud835\udcbb\u0880Eabcdefgijlnorstv\u2082\u2089\u209f\u20a5\u20b0\u20b4\u20f0\u20f5\u20fa\u20ff\u2103\u2112\u2138\u0317\u213e\u2152\u219e\u0100;l\u064d\u2087;\u6a8c\u0180cmp\u2090\u2095\u209dute;\u41f5ma\u0100;d\u209c\u1cda\u43b3;\u6a86reve;\u411f\u0100iy\u20aa\u20aerc;\u411d;\u4433ot;\u4121\u0200;lqs\u063e\u0642\u20bd\u20c9\u0180;qs\u063e\u064c\u20c4lan\xf4\u0665\u0200;cdl\u0665\u20d2\u20d5\u20e5c;\u6aa9ot\u0100;o\u20dc\u20dd\u6a80\u0100;l\u20e2\u20e3\u6a82;\u6a84\u0100;e\u20ea\u20ed\uc000\u22db\ufe00s;\u6a94r;\uc000\ud835\udd24\u0100;g\u0673\u061bmel;\u6137cy;\u4453\u0200;Eaj\u065a\u210c\u210e\u2110;\u6a92;\u6aa5;\u6aa4\u0200Eaes\u211b\u211d\u2129\u2134;\u6269p\u0100;p\u2123\u2124\u6a8arox\xbb\u2124\u0100;q\u212e\u212f\u6a88\u0100;q\u212e\u211bim;\u62e7pf;\uc000\ud835\udd58\u0100ci\u2143\u2146r;\u610am\u0180;el\u066b\u214e\u2150;\u6a8e;\u6a90\u8300>;cdlqr\u05ee\u2160\u216a\u216e\u2173\u2179\u0100ci\u2165\u2167;\u6aa7r;\u6a7aot;\u62d7Par;\u6995uest;\u6a7c\u0280adels\u2184\u216a\u2190\u0656\u219b\u01f0\u2189\0\u218epro\xf8\u209er;\u6978q\u0100lq\u063f\u2196les\xf3\u2088i\xed\u066b\u0100en\u21a3\u21adrtneqq;\uc000\u2269\ufe00\xc5\u21aa\u0500Aabcefkosy\u21c4\u21c7\u21f1\u21f5\u21fa\u2218\u221d\u222f\u2268\u227dr\xf2\u03a0\u0200ilmr\u21d0\u21d4\u21d7\u21dbrs\xf0\u1484f\xbb\u2024il\xf4\u06a9\u0100dr\u21e0\u21e4cy;\u444a\u0180;cw\u08f4\u21eb\u21efir;\u6948;\u61adar;\u610firc;\u4125\u0180alr\u2201\u220e\u2213rts\u0100;u\u2209\u220a\u6665it\xbb\u220alip;\u6026con;\u62b9r;\uc000\ud835\udd25s\u0100ew\u2223\u2229arow;\u6925arow;\u6926\u0280amopr\u223a\u223e\u2243\u225e\u2263rr;\u61fftht;\u623bk\u0100lr\u2249\u2253eftarrow;\u61a9ightarrow;\u61aaf;\uc000\ud835\udd59bar;\u6015\u0180clt\u226f\u2274\u2278r;\uc000\ud835\udcbdas\xe8\u21f4rok;\u4127\u0100bp\u2282\u2287ull;\u6043hen\xbb\u1c5b\u0ae1\u22a3\0\u22aa\0\u22b8\u22c5\u22ce\0\u22d5\u22f3\0\0\u22f8\u2322\u2367\u2362\u237f\0\u2386\u23aa\u23b4cute\u803b\xed\u40ed\u0180;iy\u0771\u22b0\u22b5rc\u803b\xee\u40ee;\u4438\u0100cx\u22bc\u22bfy;\u4435cl\u803b\xa1\u40a1\u0100fr\u039f\u22c9;\uc000\ud835\udd26rave\u803b\xec\u40ec\u0200;ino\u073e\u22dd\u22e9\u22ee\u0100in\u22e2\u22e6nt;\u6a0ct;\u622dfin;\u69dcta;\u6129lig;\u4133\u0180aop\u22fe\u231a\u231d\u0180cgt\u2305\u2308\u2317r;\u412b\u0180elp\u071f\u230f\u2313in\xe5\u078ear\xf4\u0720h;\u4131f;\u62b7ed;\u41b5\u0280;cfot\u04f4\u232c\u2331\u233d\u2341are;\u6105in\u0100;t\u2338\u2339\u621eie;\u69dddo\xf4\u2319\u0280;celp\u0757\u234c\u2350\u235b\u2361al;\u62ba\u0100gr\u2355\u2359er\xf3\u1563\xe3\u234darhk;\u6a17rod;\u6a3c\u0200cgpt\u236f\u2372\u2376\u237by;\u4451on;\u412ff;\uc000\ud835\udd5aa;\u43b9uest\u803b\xbf\u40bf\u0100ci\u238a\u238fr;\uc000\ud835\udcben\u0280;Edsv\u04f4\u239b\u239d\u23a1\u04f3;\u62f9ot;\u62f5\u0100;v\u23a6\u23a7\u62f4;\u62f3\u0100;i\u0777\u23aelde;\u4129\u01eb\u23b8\0\u23bccy;\u4456l\u803b\xef\u40ef\u0300cfmosu\u23cc\u23d7\u23dc\u23e1\u23e7\u23f5\u0100iy\u23d1\u23d5rc;\u4135;\u4439r;\uc000\ud835\udd27ath;\u4237pf;\uc000\ud835\udd5b\u01e3\u23ec\0\u23f1r;\uc000\ud835\udcbfrcy;\u4458kcy;\u4454\u0400acfghjos\u240b\u2416\u2422\u2427\u242d\u2431\u2435\u243bppa\u0100;v\u2413\u2414\u43ba;\u43f0\u0100ey\u241b\u2420dil;\u4137;\u443ar;\uc000\ud835\udd28reen;\u4138cy;\u4445cy;\u445cpf;\uc000\ud835\udd5ccr;\uc000\ud835\udcc0\u0b80ABEHabcdefghjlmnoprstuv\u2470\u2481\u2486\u248d\u2491\u250e\u253d\u255a\u2580\u264e\u265e\u2665\u2679\u267d\u269a\u26b2\u26d8\u275d\u2768\u278b\u27c0\u2801\u2812\u0180art\u2477\u247a\u247cr\xf2\u09c6\xf2\u0395ail;\u691barr;\u690e\u0100;g\u0994\u248b;\u6a8bar;\u6962\u0963\u24a5\0\u24aa\0\u24b1\0\0\0\0\0\u24b5\u24ba\0\u24c6\u24c8\u24cd\0\u24f9ute;\u413amptyv;\u69b4ra\xee\u084cbda;\u43bbg\u0180;dl\u088e\u24c1\u24c3;\u6991\xe5\u088e;\u6a85uo\u803b\xab\u40abr\u0400;bfhlpst\u0899\u24de\u24e6\u24e9\u24eb\u24ee\u24f1\u24f5\u0100;f\u089d\u24e3s;\u691fs;\u691d\xeb\u2252p;\u61abl;\u6939im;\u6973l;\u61a2\u0180;ae\u24ff\u2500\u2504\u6aabil;\u6919\u0100;s\u2509\u250a\u6aad;\uc000\u2aad\ufe00\u0180abr\u2515\u2519\u251drr;\u690crk;\u6772\u0100ak\u2522\u252cc\u0100ek\u2528\u252a;\u407b;\u405b\u0100es\u2531\u2533;\u698bl\u0100du\u2539\u253b;\u698f;\u698d\u0200aeuy\u2546\u254b\u2556\u2558ron;\u413e\u0100di\u2550\u2554il;\u413c\xec\u08b0\xe2\u2529;\u443b\u0200cqrs\u2563\u2566\u256d\u257da;\u6936uo\u0100;r\u0e19\u1746\u0100du\u2572\u2577har;\u6967shar;\u694bh;\u61b2\u0280;fgqs\u258b\u258c\u0989\u25f3\u25ff\u6264t\u0280ahlrt\u2598\u25a4\u25b7\u25c2\u25e8rrow\u0100;t\u0899\u25a1a\xe9\u24f6arpoon\u0100du\u25af\u25b4own\xbb\u045ap\xbb\u0966eftarrows;\u61c7ight\u0180ahs\u25cd\u25d6\u25derrow\u0100;s\u08f4\u08a7arpoon\xf3\u0f98quigarro\xf7\u21f0hreetimes;\u62cb\u0180;qs\u258b\u0993\u25falan\xf4\u09ac\u0280;cdgs\u09ac\u260a\u260d\u261d\u2628c;\u6aa8ot\u0100;o\u2614\u2615\u6a7f\u0100;r\u261a\u261b\u6a81;\u6a83\u0100;e\u2622\u2625\uc000\u22da\ufe00s;\u6a93\u0280adegs\u2633\u2639\u263d\u2649\u264bppro\xf8\u24c6ot;\u62d6q\u0100gq\u2643\u2645\xf4\u0989gt\xf2\u248c\xf4\u099bi\xed\u09b2\u0180ilr\u2655\u08e1\u265asht;\u697c;\uc000\ud835\udd29\u0100;E\u099c\u2663;\u6a91\u0161\u2669\u2676r\u0100du\u25b2\u266e\u0100;l\u0965\u2673;\u696alk;\u6584cy;\u4459\u0280;acht\u0a48\u2688\u268b\u2691\u2696r\xf2\u25c1orne\xf2\u1d08ard;\u696bri;\u65fa\u0100io\u269f\u26a4dot;\u4140ust\u0100;a\u26ac\u26ad\u63b0che\xbb\u26ad\u0200Eaes\u26bb\u26bd\u26c9\u26d4;\u6268p\u0100;p\u26c3\u26c4\u6a89rox\xbb\u26c4\u0100;q\u26ce\u26cf\u6a87\u0100;q\u26ce\u26bbim;\u62e6\u0400abnoptwz\u26e9\u26f4\u26f7\u271a\u272f\u2741\u2747\u2750\u0100nr\u26ee\u26f1g;\u67ecr;\u61fdr\xeb\u08c1g\u0180lmr\u26ff\u270d\u2714eft\u0100ar\u09e6\u2707ight\xe1\u09f2apsto;\u67fcight\xe1\u09fdparrow\u0100lr\u2725\u2729ef\xf4\u24edight;\u61ac\u0180afl\u2736\u2739\u273dr;\u6985;\uc000\ud835\udd5dus;\u6a2dimes;\u6a34\u0161\u274b\u274fst;\u6217\xe1\u134e\u0180;ef\u2757\u2758\u1800\u65cange\xbb\u2758ar\u0100;l\u2764\u2765\u4028t;\u6993\u0280achmt\u2773\u2776\u277c\u2785\u2787r\xf2\u08a8orne\xf2\u1d8car\u0100;d\u0f98\u2783;\u696d;\u600eri;\u62bf\u0300achiqt\u2798\u279d\u0a40\u27a2\u27ae\u27bbquo;\u6039r;\uc000\ud835\udcc1m\u0180;eg\u09b2\u27aa\u27ac;\u6a8d;\u6a8f\u0100bu\u252a\u27b3o\u0100;r\u0e1f\u27b9;\u601arok;\u4142\u8400<;cdhilqr\u082b\u27d2\u2639\u27dc\u27e0\u27e5\u27ea\u27f0\u0100ci\u27d7\u27d9;\u6aa6r;\u6a79re\xe5\u25f2mes;\u62c9arr;\u6976uest;\u6a7b\u0100Pi\u27f5\u27f9ar;\u6996\u0180;ef\u2800\u092d\u181b\u65c3r\u0100du\u2807\u280dshar;\u694ahar;\u6966\u0100en\u2817\u2821rtneqq;\uc000\u2268\ufe00\xc5\u281e\u0700Dacdefhilnopsu\u2840\u2845\u2882\u288e\u2893\u28a0\u28a5\u28a8\u28da\u28e2\u28e4\u0a83\u28f3\u2902Dot;\u623a\u0200clpr\u284e\u2852\u2863\u287dr\u803b\xaf\u40af\u0100et\u2857\u2859;\u6642\u0100;e\u285e\u285f\u6720se\xbb\u285f\u0100;s\u103b\u2868to\u0200;dlu\u103b\u2873\u2877\u287bow\xee\u048cef\xf4\u090f\xf0\u13d1ker;\u65ae\u0100oy\u2887\u288cmma;\u6a29;\u443cash;\u6014asuredangle\xbb\u1626r;\uc000\ud835\udd2ao;\u6127\u0180cdn\u28af\u28b4\u28c9ro\u803b\xb5\u40b5\u0200;acd\u1464\u28bd\u28c0\u28c4s\xf4\u16a7ir;\u6af0ot\u80bb\xb7\u01b5us\u0180;bd\u28d2\u1903\u28d3\u6212\u0100;u\u1d3c\u28d8;\u6a2a\u0163\u28de\u28e1p;\u6adb\xf2\u2212\xf0\u0a81\u0100dp\u28e9\u28eeels;\u62a7f;\uc000\ud835\udd5e\u0100ct\u28f8\u28fdr;\uc000\ud835\udcc2pos\xbb\u159d\u0180;lm\u2909\u290a\u290d\u43bctimap;\u62b8\u0c00GLRVabcdefghijlmoprstuvw\u2942\u2953\u297e\u2989\u2998\u29da\u29e9\u2a15\u2a1a\u2a58\u2a5d\u2a83\u2a95\u2aa4\u2aa8\u2b04\u2b07\u2b44\u2b7f\u2bae\u2c34\u2c67\u2c7c\u2ce9\u0100gt\u2947\u294b;\uc000\u22d9\u0338\u0100;v\u2950\u0bcf\uc000\u226b\u20d2\u0180elt\u295a\u2972\u2976ft\u0100ar\u2961\u2967rrow;\u61cdightarrow;\u61ce;\uc000\u22d8\u0338\u0100;v\u297b\u0c47\uc000\u226a\u20d2ightarrow;\u61cf\u0100Dd\u298e\u2993ash;\u62afash;\u62ae\u0280bcnpt\u29a3\u29a7\u29ac\u29b1\u29ccla\xbb\u02deute;\u4144g;\uc000\u2220\u20d2\u0280;Eiop\u0d84\u29bc\u29c0\u29c5\u29c8;\uc000\u2a70\u0338d;\uc000\u224b\u0338s;\u4149ro\xf8\u0d84ur\u0100;a\u29d3\u29d4\u666el\u0100;s\u29d3\u0b38\u01f3\u29df\0\u29e3p\u80bb\xa0\u0b37mp\u0100;e\u0bf9\u0c00\u0280aeouy\u29f4\u29fe\u2a03\u2a10\u2a13\u01f0\u29f9\0\u29fb;\u6a43on;\u4148dil;\u4146ng\u0100;d\u0d7e\u2a0aot;\uc000\u2a6d\u0338p;\u6a42;\u443dash;\u6013\u0380;Aadqsx\u0b92\u2a29\u2a2d\u2a3b\u2a41\u2a45\u2a50rr;\u61d7r\u0100hr\u2a33\u2a36k;\u6924\u0100;o\u13f2\u13f0ot;\uc000\u2250\u0338ui\xf6\u0b63\u0100ei\u2a4a\u2a4ear;\u6928\xed\u0b98ist\u0100;s\u0ba0\u0b9fr;\uc000\ud835\udd2b\u0200Eest\u0bc5\u2a66\u2a79\u2a7c\u0180;qs\u0bbc\u2a6d\u0be1\u0180;qs\u0bbc\u0bc5\u2a74lan\xf4\u0be2i\xed\u0bea\u0100;r\u0bb6\u2a81\xbb\u0bb7\u0180Aap\u2a8a\u2a8d\u2a91r\xf2\u2971rr;\u61aear;\u6af2\u0180;sv\u0f8d\u2a9c\u0f8c\u0100;d\u2aa1\u2aa2\u62fc;\u62facy;\u445a\u0380AEadest\u2ab7\u2aba\u2abe\u2ac2\u2ac5\u2af6\u2af9r\xf2\u2966;\uc000\u2266\u0338rr;\u619ar;\u6025\u0200;fqs\u0c3b\u2ace\u2ae3\u2aeft\u0100ar\u2ad4\u2ad9rro\xf7\u2ac1ightarro\xf7\u2a90\u0180;qs\u0c3b\u2aba\u2aealan\xf4\u0c55\u0100;s\u0c55\u2af4\xbb\u0c36i\xed\u0c5d\u0100;r\u0c35\u2afei\u0100;e\u0c1a\u0c25i\xe4\u0d90\u0100pt\u2b0c\u2b11f;\uc000\ud835\udd5f\u8180\xac;in\u2b19\u2b1a\u2b36\u40acn\u0200;Edv\u0b89\u2b24\u2b28\u2b2e;\uc000\u22f9\u0338ot;\uc000\u22f5\u0338\u01e1\u0b89\u2b33\u2b35;\u62f7;\u62f6i\u0100;v\u0cb8\u2b3c\u01e1\u0cb8\u2b41\u2b43;\u62fe;\u62fd\u0180aor\u2b4b\u2b63\u2b69r\u0200;ast\u0b7b\u2b55\u2b5a\u2b5flle\xec\u0b7bl;\uc000\u2afd\u20e5;\uc000\u2202\u0338lint;\u6a14\u0180;ce\u0c92\u2b70\u2b73u\xe5\u0ca5\u0100;c\u0c98\u2b78\u0100;e\u0c92\u2b7d\xf1\u0c98\u0200Aait\u2b88\u2b8b\u2b9d\u2ba7r\xf2\u2988rr\u0180;cw\u2b94\u2b95\u2b99\u619b;\uc000\u2933\u0338;\uc000\u219d\u0338ghtarrow\xbb\u2b95ri\u0100;e\u0ccb\u0cd6\u0380chimpqu\u2bbd\u2bcd\u2bd9\u2b04\u0b78\u2be4\u2bef\u0200;cer\u0d32\u2bc6\u0d37\u2bc9u\xe5\u0d45;\uc000\ud835\udcc3ort\u026d\u2b05\0\0\u2bd6ar\xe1\u2b56m\u0100;e\u0d6e\u2bdf\u0100;q\u0d74\u0d73su\u0100bp\u2beb\u2bed\xe5\u0cf8\xe5\u0d0b\u0180bcp\u2bf6\u2c11\u2c19\u0200;Ees\u2bff\u2c00\u0d22\u2c04\u6284;\uc000\u2ac5\u0338et\u0100;e\u0d1b\u2c0bq\u0100;q\u0d23\u2c00c\u0100;e\u0d32\u2c17\xf1\u0d38\u0200;Ees\u2c22\u2c23\u0d5f\u2c27\u6285;\uc000\u2ac6\u0338et\u0100;e\u0d58\u2c2eq\u0100;q\u0d60\u2c23\u0200gilr\u2c3d\u2c3f\u2c45\u2c47\xec\u0bd7lde\u803b\xf1\u40f1\xe7\u0c43iangle\u0100lr\u2c52\u2c5ceft\u0100;e\u0c1a\u2c5a\xf1\u0c26ight\u0100;e\u0ccb\u2c65\xf1\u0cd7\u0100;m\u2c6c\u2c6d\u43bd\u0180;es\u2c74\u2c75\u2c79\u4023ro;\u6116p;\u6007\u0480DHadgilrs\u2c8f\u2c94\u2c99\u2c9e\u2ca3\u2cb0\u2cb6\u2cd3\u2ce3ash;\u62adarr;\u6904p;\uc000\u224d\u20d2ash;\u62ac\u0100et\u2ca8\u2cac;\uc000\u2265\u20d2;\uc000>\u20d2nfin;\u69de\u0180Aet\u2cbd\u2cc1\u2cc5rr;\u6902;\uc000\u2264\u20d2\u0100;r\u2cca\u2ccd\uc000<\u20d2ie;\uc000\u22b4\u20d2\u0100At\u2cd8\u2cdcrr;\u6903rie;\uc000\u22b5\u20d2im;\uc000\u223c\u20d2\u0180Aan\u2cf0\u2cf4\u2d02rr;\u61d6r\u0100hr\u2cfa\u2cfdk;\u6923\u0100;o\u13e7\u13e5ear;\u6927\u1253\u1a95\0\0\0\0\0\0\0\0\0\0\0\0\0\u2d2d\0\u2d38\u2d48\u2d60\u2d65\u2d72\u2d84\u1b07\0\0\u2d8d\u2dab\0\u2dc8\u2dce\0\u2ddc\u2e19\u2e2b\u2e3e\u2e43\u0100cs\u2d31\u1a97ute\u803b\xf3\u40f3\u0100iy\u2d3c\u2d45r\u0100;c\u1a9e\u2d42\u803b\xf4\u40f4;\u443e\u0280abios\u1aa0\u2d52\u2d57\u01c8\u2d5alac;\u4151v;\u6a38old;\u69bclig;\u4153\u0100cr\u2d69\u2d6dir;\u69bf;\uc000\ud835\udd2c\u036f\u2d79\0\0\u2d7c\0\u2d82n;\u42dbave\u803b\xf2\u40f2;\u69c1\u0100bm\u2d88\u0df4ar;\u69b5\u0200acit\u2d95\u2d98\u2da5\u2da8r\xf2\u1a80\u0100ir\u2d9d\u2da0r;\u69beoss;\u69bbn\xe5\u0e52;\u69c0\u0180aei\u2db1\u2db5\u2db9cr;\u414dga;\u43c9\u0180cdn\u2dc0\u2dc5\u01cdron;\u43bf;\u69b6pf;\uc000\ud835\udd60\u0180ael\u2dd4\u2dd7\u01d2r;\u69b7rp;\u69b9\u0380;adiosv\u2dea\u2deb\u2dee\u2e08\u2e0d\u2e10\u2e16\u6228r\xf2\u1a86\u0200;efm\u2df7\u2df8\u2e02\u2e05\u6a5dr\u0100;o\u2dfe\u2dff\u6134f\xbb\u2dff\u803b\xaa\u40aa\u803b\xba\u40bagof;\u62b6r;\u6a56lope;\u6a57;\u6a5b\u0180clo\u2e1f\u2e21\u2e27\xf2\u2e01ash\u803b\xf8\u40f8l;\u6298i\u016c\u2e2f\u2e34de\u803b\xf5\u40f5es\u0100;a\u01db\u2e3as;\u6a36ml\u803b\xf6\u40f6bar;\u633d\u0ae1\u2e5e\0\u2e7d\0\u2e80\u2e9d\0\u2ea2\u2eb9\0\0\u2ecb\u0e9c\0\u2f13\0\0\u2f2b\u2fbc\0\u2fc8r\u0200;ast\u0403\u2e67\u2e72\u0e85\u8100\xb6;l\u2e6d\u2e6e\u40b6le\xec\u0403\u0269\u2e78\0\0\u2e7bm;\u6af3;\u6afdy;\u443fr\u0280cimpt\u2e8b\u2e8f\u2e93\u1865\u2e97nt;\u4025od;\u402eil;\u6030enk;\u6031r;\uc000\ud835\udd2d\u0180imo\u2ea8\u2eb0\u2eb4\u0100;v\u2ead\u2eae\u43c6;\u43d5ma\xf4\u0a76ne;\u660e\u0180;tv\u2ebf\u2ec0\u2ec8\u43c0chfork\xbb\u1ffd;\u43d6\u0100au\u2ecf\u2edfn\u0100ck\u2ed5\u2eddk\u0100;h\u21f4\u2edb;\u610e\xf6\u21f4s\u0480;abcdemst\u2ef3\u2ef4\u1908\u2ef9\u2efd\u2f04\u2f06\u2f0a\u2f0e\u402bcir;\u6a23ir;\u6a22\u0100ou\u1d40\u2f02;\u6a25;\u6a72n\u80bb\xb1\u0e9dim;\u6a26wo;\u6a27\u0180ipu\u2f19\u2f20\u2f25ntint;\u6a15f;\uc000\ud835\udd61nd\u803b\xa3\u40a3\u0500;Eaceinosu\u0ec8\u2f3f\u2f41\u2f44\u2f47\u2f81\u2f89\u2f92\u2f7e\u2fb6;\u6ab3p;\u6ab7u\xe5\u0ed9\u0100;c\u0ece\u2f4c\u0300;acens\u0ec8\u2f59\u2f5f\u2f66\u2f68\u2f7eppro\xf8\u2f43urlye\xf1\u0ed9\xf1\u0ece\u0180aes\u2f6f\u2f76\u2f7approx;\u6ab9qq;\u6ab5im;\u62e8i\xed\u0edfme\u0100;s\u2f88\u0eae\u6032\u0180Eas\u2f78\u2f90\u2f7a\xf0\u2f75\u0180dfp\u0eec\u2f99\u2faf\u0180als\u2fa0\u2fa5\u2faalar;\u632eine;\u6312urf;\u6313\u0100;t\u0efb\u2fb4\xef\u0efbrel;\u62b0\u0100ci\u2fc0\u2fc5r;\uc000\ud835\udcc5;\u43c8ncsp;\u6008\u0300fiopsu\u2fda\u22e2\u2fdf\u2fe5\u2feb\u2ff1r;\uc000\ud835\udd2epf;\uc000\ud835\udd62rime;\u6057cr;\uc000\ud835\udcc6\u0180aeo\u2ff8\u3009\u3013t\u0100ei\u2ffe\u3005rnion\xf3\u06b0nt;\u6a16st\u0100;e\u3010\u3011\u403f\xf1\u1f19\xf4\u0f14\u0a80ABHabcdefhilmnoprstux\u3040\u3051\u3055\u3059\u30e0\u310e\u312b\u3147\u3162\u3172\u318e\u3206\u3215\u3224\u3229\u3258\u326e\u3272\u3290\u32b0\u32b7\u0180art\u3047\u304a\u304cr\xf2\u10b3\xf2\u03ddail;\u691car\xf2\u1c65ar;\u6964\u0380cdenqrt\u3068\u3075\u3078\u307f\u308f\u3094\u30cc\u0100eu\u306d\u3071;\uc000\u223d\u0331te;\u4155i\xe3\u116emptyv;\u69b3g\u0200;del\u0fd1\u3089\u308b\u308d;\u6992;\u69a5\xe5\u0fd1uo\u803b\xbb\u40bbr\u0580;abcfhlpstw\u0fdc\u30ac\u30af\u30b7\u30b9\u30bc\u30be\u30c0\u30c3\u30c7\u30cap;\u6975\u0100;f\u0fe0\u30b4s;\u6920;\u6933s;\u691e\xeb\u225d\xf0\u272el;\u6945im;\u6974l;\u61a3;\u619d\u0100ai\u30d1\u30d5il;\u691ao\u0100;n\u30db\u30dc\u6236al\xf3\u0f1e\u0180abr\u30e7\u30ea\u30eer\xf2\u17e5rk;\u6773\u0100ak\u30f3\u30fdc\u0100ek\u30f9\u30fb;\u407d;\u405d\u0100es\u3102\u3104;\u698cl\u0100du\u310a\u310c;\u698e;\u6990\u0200aeuy\u3117\u311c\u3127\u3129ron;\u4159\u0100di\u3121\u3125il;\u4157\xec\u0ff2\xe2\u30fa;\u4440\u0200clqs\u3134\u3137\u313d\u3144a;\u6937dhar;\u6969uo\u0100;r\u020e\u020dh;\u61b3\u0180acg\u314e\u315f\u0f44l\u0200;ips\u0f78\u3158\u315b\u109cn\xe5\u10bbar\xf4\u0fa9t;\u65ad\u0180ilr\u3169\u1023\u316esht;\u697d;\uc000\ud835\udd2f\u0100ao\u3177\u3186r\u0100du\u317d\u317f\xbb\u047b\u0100;l\u1091\u3184;\u696c\u0100;v\u318b\u318c\u43c1;\u43f1\u0180gns\u3195\u31f9\u31fcht\u0300ahlrst\u31a4\u31b0\u31c2\u31d8\u31e4\u31eerrow\u0100;t\u0fdc\u31ada\xe9\u30c8arpoon\u0100du\u31bb\u31bfow\xee\u317ep\xbb\u1092eft\u0100ah\u31ca\u31d0rrow\xf3\u0feaarpoon\xf3\u0551ightarrows;\u61c9quigarro\xf7\u30cbhreetimes;\u62ccg;\u42daingdotse\xf1\u1f32\u0180ahm\u320d\u3210\u3213r\xf2\u0feaa\xf2\u0551;\u600foust\u0100;a\u321e\u321f\u63b1che\xbb\u321fmid;\u6aee\u0200abpt\u3232\u323d\u3240\u3252\u0100nr\u3237\u323ag;\u67edr;\u61fer\xeb\u1003\u0180afl\u3247\u324a\u324er;\u6986;\uc000\ud835\udd63us;\u6a2eimes;\u6a35\u0100ap\u325d\u3267r\u0100;g\u3263\u3264\u4029t;\u6994olint;\u6a12ar\xf2\u31e3\u0200achq\u327b\u3280\u10bc\u3285quo;\u603ar;\uc000\ud835\udcc7\u0100bu\u30fb\u328ao\u0100;r\u0214\u0213\u0180hir\u3297\u329b\u32a0re\xe5\u31f8mes;\u62cai\u0200;efl\u32aa\u1059\u1821\u32ab\u65b9tri;\u69celuhar;\u6968;\u611e\u0d61\u32d5\u32db\u32df\u332c\u3338\u3371\0\u337a\u33a4\0\0\u33ec\u33f0\0\u3428\u3448\u345a\u34ad\u34b1\u34ca\u34f1\0\u3616\0\0\u3633cute;\u415bqu\xef\u27ba\u0500;Eaceinpsy\u11ed\u32f3\u32f5\u32ff\u3302\u330b\u330f\u331f\u3326\u3329;\u6ab4\u01f0\u32fa\0\u32fc;\u6ab8on;\u4161u\xe5\u11fe\u0100;d\u11f3\u3307il;\u415frc;\u415d\u0180Eas\u3316\u3318\u331b;\u6ab6p;\u6abaim;\u62e9olint;\u6a13i\xed\u1204;\u4441ot\u0180;be\u3334\u1d47\u3335\u62c5;\u6a66\u0380Aacmstx\u3346\u334a\u3357\u335b\u335e\u3363\u336drr;\u61d8r\u0100hr\u3350\u3352\xeb\u2228\u0100;o\u0a36\u0a34t\u803b\xa7\u40a7i;\u403bwar;\u6929m\u0100in\u3369\xf0nu\xf3\xf1t;\u6736r\u0100;o\u3376\u2055\uc000\ud835\udd30\u0200acoy\u3382\u3386\u3391\u33a0rp;\u666f\u0100hy\u338b\u338fcy;\u4449;\u4448rt\u026d\u3399\0\0\u339ci\xe4\u1464ara\xec\u2e6f\u803b\xad\u40ad\u0100gm\u33a8\u33b4ma\u0180;fv\u33b1\u33b2\u33b2\u43c3;\u43c2\u0400;deglnpr\u12ab\u33c5\u33c9\u33ce\u33d6\u33de\u33e1\u33e6ot;\u6a6a\u0100;q\u12b1\u12b0\u0100;E\u33d3\u33d4\u6a9e;\u6aa0\u0100;E\u33db\u33dc\u6a9d;\u6a9fe;\u6246lus;\u6a24arr;\u6972ar\xf2\u113d\u0200aeit\u33f8\u3408\u340f\u3417\u0100ls\u33fd\u3404lsetm\xe9\u336ahp;\u6a33parsl;\u69e4\u0100dl\u1463\u3414e;\u6323\u0100;e\u341c\u341d\u6aaa\u0100;s\u3422\u3423\u6aac;\uc000\u2aac\ufe00\u0180flp\u342e\u3433\u3442tcy;\u444c\u0100;b\u3438\u3439\u402f\u0100;a\u343e\u343f\u69c4r;\u633ff;\uc000\ud835\udd64a\u0100dr\u344d\u0402es\u0100;u\u3454\u3455\u6660it\xbb\u3455\u0180csu\u3460\u3479\u349f\u0100au\u3465\u346fp\u0100;s\u1188\u346b;\uc000\u2293\ufe00p\u0100;s\u11b4\u3475;\uc000\u2294\ufe00u\u0100bp\u347f\u348f\u0180;es\u1197\u119c\u3486et\u0100;e\u1197\u348d\xf1\u119d\u0180;es\u11a8\u11ad\u3496et\u0100;e\u11a8\u349d\xf1\u11ae\u0180;af\u117b\u34a6\u05b0r\u0165\u34ab\u05b1\xbb\u117car\xf2\u1148\u0200cemt\u34b9\u34be\u34c2\u34c5r;\uc000\ud835\udcc8tm\xee\xf1i\xec\u3415ar\xe6\u11be\u0100ar\u34ce\u34d5r\u0100;f\u34d4\u17bf\u6606\u0100an\u34da\u34edight\u0100ep\u34e3\u34eapsilo\xee\u1ee0h\xe9\u2eafs\xbb\u2852\u0280bcmnp\u34fb\u355e\u1209\u358b\u358e\u0480;Edemnprs\u350e\u350f\u3511\u3515\u351e\u3523\u352c\u3531\u3536\u6282;\u6ac5ot;\u6abd\u0100;d\u11da\u351aot;\u6ac3ult;\u6ac1\u0100Ee\u3528\u352a;\u6acb;\u628alus;\u6abfarr;\u6979\u0180eiu\u353d\u3552\u3555t\u0180;en\u350e\u3545\u354bq\u0100;q\u11da\u350feq\u0100;q\u352b\u3528m;\u6ac7\u0100bp\u355a\u355c;\u6ad5;\u6ad3c\u0300;acens\u11ed\u356c\u3572\u3579\u357b\u3326ppro\xf8\u32faurlye\xf1\u11fe\xf1\u11f3\u0180aes\u3582\u3588\u331bppro\xf8\u331aq\xf1\u3317g;\u666a\u0680123;Edehlmnps\u35a9\u35ac\u35af\u121c\u35b2\u35b4\u35c0\u35c9\u35d5\u35da\u35df\u35e8\u35ed\u803b\xb9\u40b9\u803b\xb2\u40b2\u803b\xb3\u40b3;\u6ac6\u0100os\u35b9\u35bct;\u6abeub;\u6ad8\u0100;d\u1222\u35c5ot;\u6ac4s\u0100ou\u35cf\u35d2l;\u67c9b;\u6ad7arr;\u697bult;\u6ac2\u0100Ee\u35e4\u35e6;\u6acc;\u628blus;\u6ac0\u0180eiu\u35f4\u3609\u360ct\u0180;en\u121c\u35fc\u3602q\u0100;q\u1222\u35b2eq\u0100;q\u35e7\u35e4m;\u6ac8\u0100bp\u3611\u3613;\u6ad4;\u6ad6\u0180Aan\u361c\u3620\u362drr;\u61d9r\u0100hr\u3626\u3628\xeb\u222e\u0100;o\u0a2b\u0a29war;\u692alig\u803b\xdf\u40df\u0be1\u3651\u365d\u3660\u12ce\u3673\u3679\0\u367e\u36c2\0\0\0\0\0\u36db\u3703\0\u3709\u376c\0\0\0\u3787\u0272\u3656\0\0\u365bget;\u6316;\u43c4r\xeb\u0e5f\u0180aey\u3666\u366b\u3670ron;\u4165dil;\u4163;\u4442lrec;\u6315r;\uc000\ud835\udd31\u0200eiko\u3686\u369d\u36b5\u36bc\u01f2\u368b\0\u3691e\u01004f\u1284\u1281a\u0180;sv\u3698\u3699\u369b\u43b8ym;\u43d1\u0100cn\u36a2\u36b2k\u0100as\u36a8\u36aeppro\xf8\u12c1im\xbb\u12acs\xf0\u129e\u0100as\u36ba\u36ae\xf0\u12c1rn\u803b\xfe\u40fe\u01ec\u031f\u36c6\u22e7es\u8180\xd7;bd\u36cf\u36d0\u36d8\u40d7\u0100;a\u190f\u36d5r;\u6a31;\u6a30\u0180eps\u36e1\u36e3\u3700\xe1\u2a4d\u0200;bcf\u0486\u36ec\u36f0\u36f4ot;\u6336ir;\u6af1\u0100;o\u36f9\u36fc\uc000\ud835\udd65rk;\u6ada\xe1\u3362rime;\u6034\u0180aip\u370f\u3712\u3764d\xe5\u1248\u0380adempst\u3721\u374d\u3740\u3751\u3757\u375c\u375fngle\u0280;dlqr\u3730\u3731\u3736\u3740\u3742\u65b5own\xbb\u1dbbeft\u0100;e\u2800\u373e\xf1\u092e;\u625cight\u0100;e\u32aa\u374b\xf1\u105aot;\u65ecinus;\u6a3alus;\u6a39b;\u69cdime;\u6a3bezium;\u63e2\u0180cht\u3772\u377d\u3781\u0100ry\u3777\u377b;\uc000\ud835\udcc9;\u4446cy;\u445brok;\u4167\u0100io\u378b\u378ex\xf4\u1777head\u0100lr\u3797\u37a0eftarro\xf7\u084fightarrow\xbb\u0f5d\u0900AHabcdfghlmoprstuw\u37d0\u37d3\u37d7\u37e4\u37f0\u37fc\u380e\u381c\u3823\u3834\u3851\u385d\u386b\u38a9\u38cc\u38d2\u38ea\u38f6r\xf2\u03edar;\u6963\u0100cr\u37dc\u37e2ute\u803b\xfa\u40fa\xf2\u1150r\u01e3\u37ea\0\u37edy;\u445eve;\u416d\u0100iy\u37f5\u37farc\u803b\xfb\u40fb;\u4443\u0180abh\u3803\u3806\u380br\xf2\u13adlac;\u4171a\xf2\u13c3\u0100ir\u3813\u3818sht;\u697e;\uc000\ud835\udd32rave\u803b\xf9\u40f9\u0161\u3827\u3831r\u0100lr\u382c\u382e\xbb\u0957\xbb\u1083lk;\u6580\u0100ct\u3839\u384d\u026f\u383f\0\0\u384arn\u0100;e\u3845\u3846\u631cr\xbb\u3846op;\u630fri;\u65f8\u0100al\u3856\u385acr;\u416b\u80bb\xa8\u0349\u0100gp\u3862\u3866on;\u4173f;\uc000\ud835\udd66\u0300adhlsu\u114b\u3878\u387d\u1372\u3891\u38a0own\xe1\u13b3arpoon\u0100lr\u3888\u388cef\xf4\u382digh\xf4\u382fi\u0180;hl\u3899\u389a\u389c\u43c5\xbb\u13faon\xbb\u389aparrows;\u61c8\u0180cit\u38b0\u38c4\u38c8\u026f\u38b6\0\0\u38c1rn\u0100;e\u38bc\u38bd\u631dr\xbb\u38bdop;\u630eng;\u416fri;\u65f9cr;\uc000\ud835\udcca\u0180dir\u38d9\u38dd\u38e2ot;\u62f0lde;\u4169i\u0100;f\u3730\u38e8\xbb\u1813\u0100am\u38ef\u38f2r\xf2\u38a8l\u803b\xfc\u40fcangle;\u69a7\u0780ABDacdeflnoprsz\u391c\u391f\u3929\u392d\u39b5\u39b8\u39bd\u39df\u39e4\u39e8\u39f3\u39f9\u39fd\u3a01\u3a20r\xf2\u03f7ar\u0100;v\u3926\u3927\u6ae8;\u6ae9as\xe8\u03e1\u0100nr\u3932\u3937grt;\u699c\u0380eknprst\u34e3\u3946\u394b\u3952\u395d\u3964\u3996app\xe1\u2415othin\xe7\u1e96\u0180hir\u34eb\u2ec8\u3959op\xf4\u2fb5\u0100;h\u13b7\u3962\xef\u318d\u0100iu\u3969\u396dgm\xe1\u33b3\u0100bp\u3972\u3984setneq\u0100;q\u397d\u3980\uc000\u228a\ufe00;\uc000\u2acb\ufe00setneq\u0100;q\u398f\u3992\uc000\u228b\ufe00;\uc000\u2acc\ufe00\u0100hr\u399b\u399fet\xe1\u369ciangle\u0100lr\u39aa\u39afeft\xbb\u0925ight\xbb\u1051y;\u4432ash\xbb\u1036\u0180elr\u39c4\u39d2\u39d7\u0180;be\u2dea\u39cb\u39cfar;\u62bbq;\u625alip;\u62ee\u0100bt\u39dc\u1468a\xf2\u1469r;\uc000\ud835\udd33tr\xe9\u39aesu\u0100bp\u39ef\u39f1\xbb\u0d1c\xbb\u0d59pf;\uc000\ud835\udd67ro\xf0\u0efbtr\xe9\u39b4\u0100cu\u3a06\u3a0br;\uc000\ud835\udccb\u0100bp\u3a10\u3a18n\u0100Ee\u3980\u3a16\xbb\u397en\u0100Ee\u3992\u3a1e\xbb\u3990igzag;\u699a\u0380cefoprs\u3a36\u3a3b\u3a56\u3a5b\u3a54\u3a61\u3a6airc;\u4175\u0100di\u3a40\u3a51\u0100bg\u3a45\u3a49ar;\u6a5fe\u0100;q\u15fa\u3a4f;\u6259erp;\u6118r;\uc000\ud835\udd34pf;\uc000\ud835\udd68\u0100;e\u1479\u3a66at\xe8\u1479cr;\uc000\ud835\udccc\u0ae3\u178e\u3a87\0\u3a8b\0\u3a90\u3a9b\0\0\u3a9d\u3aa8\u3aab\u3aaf\0\0\u3ac3\u3ace\0\u3ad8\u17dc\u17dftr\xe9\u17d1r;\uc000\ud835\udd35\u0100Aa\u3a94\u3a97r\xf2\u03c3r\xf2\u09f6;\u43be\u0100Aa\u3aa1\u3aa4r\xf2\u03b8r\xf2\u09eba\xf0\u2713is;\u62fb\u0180dpt\u17a4\u3ab5\u3abe\u0100fl\u3aba\u17a9;\uc000\ud835\udd69im\xe5\u17b2\u0100Aa\u3ac7\u3acar\xf2\u03cer\xf2\u0a01\u0100cq\u3ad2\u17b8r;\uc000\ud835\udccd\u0100pt\u17d6\u3adcr\xe9\u17d4\u0400acefiosu\u3af0\u3afd\u3b08\u3b0c\u3b11\u3b15\u3b1b\u3b21c\u0100uy\u3af6\u3afbte\u803b\xfd\u40fd;\u444f\u0100iy\u3b02\u3b06rc;\u4177;\u444bn\u803b\xa5\u40a5r;\uc000\ud835\udd36cy;\u4457pf;\uc000\ud835\udd6acr;\uc000\ud835\udcce\u0100cm\u3b26\u3b29y;\u444el\u803b\xff\u40ff\u0500acdefhiosw\u3b42\u3b48\u3b54\u3b58\u3b64\u3b69\u3b6d\u3b74\u3b7a\u3b80cute;\u417a\u0100ay\u3b4d\u3b52ron;\u417e;\u4437ot;\u417c\u0100et\u3b5d\u3b61tr\xe6\u155fa;\u43b6r;\uc000\ud835\udd37cy;\u4436grarr;\u61ddpf;\uc000\ud835\udd6bcr;\uc000\ud835\udccf\u0100jn\u3b85\u3b87;\u600dj;\u600c'.split("").map((t=>t.charCodeAt(0)))),w=new Uint16Array("\u0200aglq\t\x15\x18\x1b\u026d\x0f\0\0\x12p;\u4026os;\u4027t;\u403et;\u403cuot;\u4022".split("").map((t=>t.charCodeAt(0))));const v=new Map([[0,65533],[128,8364],[130,8218],[131,402],[132,8222],[133,8230],[134,8224],[135,8225],[136,710],[137,8240],[138,352],[139,8249],[140,338],[142,381],[145,8216],[146,8217],[147,8220],[148,8221],[149,8226],[150,8211],[151,8212],[152,732],[153,8482],[154,353],[155,8250],[156,339],[158,382],[159,376]]),z=null!==(k=String.fromCodePoint)&&void 0!==k?k:function(t){let e="";return t>65535&&(t-=65536,e+=String.fromCharCode(t>>>10&1023|55296),t=56320|1023&t),e+=String.fromCharCode(t),e};var S;!function(t){t[t.NUM=35]="NUM",t[t.SEMI=59]="SEMI",t[t.EQUALS=61]="EQUALS",t[t.ZERO=48]="ZERO",t[t.NINE=57]="NINE",t[t.LOWER_A=97]="LOWER_A",t[t.LOWER_F=102]="LOWER_F",t[t.LOWER_X=120]="LOWER_X",t[t.LOWER_Z=122]="LOWER_Z",t[t.UPPER_A=65]="UPPER_A",t[t.UPPER_F=70]="UPPER_F",t[t.UPPER_Z=90]="UPPER_Z"}(S||(S={}));var q,B,L;function I(t){return t>=S.ZERO&&t<=S.NINE}function M(t){return t>=S.UPPER_A&&t<=S.UPPER_F||t>=S.LOWER_A&&t<=S.LOWER_F}function T(t){return t===S.EQUALS||function(t){return t>=S.UPPER_A&&t<=S.UPPER_Z||t>=S.LOWER_A&&t<=S.LOWER_Z||I(t)}(t)}!function(t){t[t.VALUE_LENGTH=49152]="VALUE_LENGTH",t[t.BRANCH_LENGTH=16256]="BRANCH_LENGTH",t[t.JUMP_TABLE=127]="JUMP_TABLE"}(q||(q={})),function(t){t[t.EntityStart=0]="EntityStart",t[t.NumericStart=1]="NumericStart",t[t.NumericDecimal=2]="NumericDecimal",t[t.NumericHex=3]="NumericHex",t[t.NamedEntity=4]="NamedEntity"}(B||(B={})),function(t){t[t.Legacy=0]="Legacy",t[t.Strict=1]="Strict",t[t.Attribute=2]="Attribute"}(L||(L={}));class R{constructor(t,e,r){this.decodeTree=t,this.emitCodePoint=e,this.errors=r,this.state=B.EntityStart,this.consumed=1,this.result=0,this.treeIndex=0,this.excess=1,this.decodeMode=L.Strict}startEntity(t){this.decodeMode=t,this.state=B.EntityStart,this.result=0,this.treeIndex=0,this.excess=1,this.consumed=1}write(t,e){switch(this.state){case B.EntityStart:return t.charCodeAt(e)===S.NUM?(this.state=B.NumericStart,this.consumed+=1,this.stateNumericStart(t,e+1)):(this.state=B.NamedEntity,this.stateNamedEntity(t,e));case B.NumericStart:return this.stateNumericStart(t,e);case B.NumericDecimal:return this.stateNumericDecimal(t,e);case B.NumericHex:return this.stateNumericHex(t,e);case B.NamedEntity:return this.stateNamedEntity(t,e)}}stateNumericStart(t,e){return e>=t.length?-1:(32|t.charCodeAt(e))===S.LOWER_X?(this.state=B.NumericHex,this.consumed+=1,this.stateNumericHex(t,e+1)):(this.state=B.NumericDecimal,this.stateNumericDecimal(t,e))}addToNumericResult(t,e,r,n){if(e!==r){const s=r-e;this.result=this.result*Math.pow(n,s)+parseInt(t.substr(e,s),n),this.consumed+=s}}stateNumericHex(t,e){const r=e;for(;e=55296&&t<=57343||t>1114111?65533:null!==(e=v.get(t))&&void 0!==e?e:t}(this.result),this.consumed),this.errors&&(t!==S.SEMI&&this.errors.missingSemicolonAfterCharacterReference(),this.errors.validateNumericCharacterReference(this.result)),this.consumed}stateNamedEntity(t,e){const{decodeTree:r}=this;let n=r[this.treeIndex],s=(n&q.VALUE_LENGTH)>>14;for(;e>14,0!==s){if(i===S.SEMI)return this.emitNamedEntityData(this.treeIndex,s,this.consumed+this.excess);this.decodeMode!==L.Strict&&(this.result=this.treeIndex,this.consumed+=this.excess,this.excess=0)}}return-1}emitNotTerminatedNamedEntity(){var t;const{result:e,decodeTree:r}=this,n=(r[e]&q.VALUE_LENGTH)>>14;return this.emitNamedEntityData(e,n,this.consumed),null===(t=this.errors)||void 0===t||t.missingSemicolonAfterCharacterReference(),this.consumed}emitNamedEntityData(t,e,r){const{decodeTree:n}=this;return this.emitCodePoint(1===e?n[t]&~q.VALUE_LENGTH:n[t+1],r),3===e&&this.emitCodePoint(n[t+2],r),r}end(){var t;switch(this.state){case B.NamedEntity:return 0===this.result||this.decodeMode===L.Attribute&&this.result!==this.treeIndex?0:this.emitNotTerminatedNamedEntity();case B.NumericDecimal:return this.emitNumericEntity(0,2);case B.NumericHex:return this.emitNumericEntity(0,3);case B.NumericStart:return null===(t=this.errors)||void 0===t||t.absenceOfDigitsInNumericCharacterReference(this.consumed),0;case B.EntityStart:return 0}}}function N(t){let e="";const r=new R(t,(t=>e+=z(t)));return function(t,n){let s=0,i=0;for(;(i=t.indexOf("&",i))>=0;){e+=t.slice(s,i),r.startEntity(n);const o=r.write(t,i+1);if(o<0){s=i+r.end();break}s=i+o,i=0===o?s+1:s}const o=e+t.slice(s);return e="",o}}function P(t,e,r,n){const s=(e&q.BRANCH_LENGTH)>>7,i=e&q.JUMP_TABLE;if(0===s)return 0!==i&&n===i?r:-1;if(i){const e=n-i;return e<0||e>=s?-1:t[r+e]-1}let o=r,u=o+s-1;for(;o<=u;){const e=o+u>>>1,r=t[e];if(rn))return t[e+s];u=e-1}}return-1}const O=N(x);function j(t,e=L.Legacy){return O(t,e)}function Z(t){return"[object String]"===function(t){return Object.prototype.toString.call(t)}(t)}N(w);const $=Object.prototype.hasOwnProperty;function U(t){return Array.prototype.slice.call(arguments,1).forEach((function(e){if(e){if("object"!=typeof e)throw new TypeError(e+"must be object");Object.keys(e).forEach((function(r){t[r]=e[r]}))}})),t}function H(t,e,r){return[].concat(t.slice(0,e),r,t.slice(e+1))}function V(t){return!(t>=55296&&t<=57343)&&(!(t>=64976&&t<=65007)&&(!!(65535&~t&&65534!=(65535&t))&&(!(t>=0&&t<=8)&&(11!==t&&(!(t>=14&&t<=31)&&(!(t>=127&&t<=159)&&!(t>1114111)))))))}function G(t){if(t>65535){const e=55296+((t-=65536)>>10),r=56320+(1023&t);return String.fromCharCode(e,r)}return String.fromCharCode(t)}const W=/\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/g,J=new RegExp(W.source+"|"+/&([a-z#][a-z0-9]{1,31});/gi.source,"gi"),Q=/^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))$/i;function X(t){return t.indexOf("\\")<0&&t.indexOf("&")<0?t:t.replace(J,(function(t,e,r){return e||function(t,e){if(35===e.charCodeAt(0)&&Q.test(e)){const r="x"===e[1].toLowerCase()?parseInt(e.slice(2),16):parseInt(e.slice(1),10);return V(r)?G(r):t}const r=j(t);return r!==t?r:t}(t,r)}))}const Y=/[&<>"]/,K=/[&<>"]/g,tt={"&":"&","<":"<",">":">",'"':"""};function et(t){return tt[t]}function rt(t){return Y.test(t)?t.replace(K,et):t}const nt=/[.?*+^$[\]\\(){}|-]/g;function st(t){switch(t){case 9:case 32:return!0}return!1}function it(t){if(t>=8192&&t<=8202)return!0;switch(t){case 9:case 10:case 11:case 12:case 13:case 32:case 160:case 5760:case 8239:case 8287:case 12288:return!0}return!1}function ot(t){return E.test(t)||A.test(t)}function ut(t){switch(t){case 33:case 34:case 35:case 36:case 37:case 38:case 39:case 40:case 41:case 42:case 43:case 44:case 45:case 46:case 47:case 58:case 59:case 60:case 61:case 62:case 63:case 64:case 91:case 92:case 93:case 94:case 95:case 96:case 123:case 124:case 125:case 126:return!0;default:return!1}}function ct(t){return t=t.trim().replace(/\s+/g," "),"\u1e7e"==="\u1e9e".toLowerCase()&&(t=t.replace(/\u1e9e/g,"\xdf")),t.toLowerCase().toUpperCase()}const at={mdurl:D,ucmicro:F};var lt=Object.freeze({__proto__:null,arrayReplaceAt:H,assign:U,escapeHtml:rt,escapeRE:function(t){return t.replace(nt,"\\$&")},fromCodePoint:G,has:function(t,e){return $.call(t,e)},isMdAsciiPunct:ut,isPunctChar:ot,isSpace:st,isString:Z,isValidEntityCode:V,isWhiteSpace:it,lib:at,normalizeReference:ct,unescapeAll:X,unescapeMd:function(t){return t.indexOf("\\")<0?t:t.replace(W,"$1")}});var ht=Object.freeze({__proto__:null,parseLinkDestination:function(t,e,r){let n,s=e;const i={ok:!1,pos:0,str:""};if(60===t.charCodeAt(s)){for(s++;s32))return i;if(41===n){if(0===o)break;o--}s++}return e===s||0!==o||(i.str=X(t.slice(e,s)),i.pos=s,i.ok=!0),i},parseLinkLabel:function(t,e,r){let n,s,i,o;const u=t.posMax,c=t.pos;for(t.pos=e+1,n=1;t.pos=r)return o;let n=t.charCodeAt(i);if(34!==n&&39!==n&&40!==n)return o;e++,i++,40===n&&(n=41),o.marker=n}for(;i"+rt(i.content)+""},pt.code_block=function(t,e,r,n,s){const i=t[e];return""+rt(t[e].content)+"
\n"},pt.fence=function(t,e,r,n,s){const i=t[e],o=i.info?X(i.info).trim():"";let u,c="",a="";if(o){const t=o.split(/(\s+)/g);c=t[0],a=t.slice(2).join("")}if(u=r.highlight&&r.highlight(i.content,c,a)||rt(i.content),0===u.indexOf("${u}
\n`}return`
${u}
\n`},pt.image=function(t,e,r,n,s){const i=t[e];return i.attrs[i.attrIndex("alt")][1]=s.renderInlineAsText(i.children,r,n),s.renderToken(t,e,r)},pt.hardbreak=function(t,e,r){return r.xhtmlOut?"
\n":"
\n"},pt.softbreak=function(t,e,r){return r.breaks?r.xhtmlOut?"
\n":"
\n":"\n"},pt.text=function(t,e){return rt(t[e].content)},pt.html_block=function(t,e){return t[e].content},pt.html_inline=function(t,e){return t[e].content},ft.prototype.renderAttrs=function(t){let e,r,n;if(!t.attrs)return"";for(n="",e=0,r=t.attrs.length;e\n":">",s},ft.prototype.renderInline=function(t,e,r){let n="";const s=this.rules;for(let i=0,o=t.length;i=0&&(r=this.attrs[e][1]),r},_t.prototype.attrJoin=function(t,e){const r=this.attrIndex(t);r<0?this.attrPush([t,e]):this.attrs[r][1]=this.attrs[r][1]+" "+e},mt.prototype.Token=_t;const gt=/\r\n?|\n/g,kt=/\0/g;function Dt(t){return/^<\/a\s*>/i.test(t)}const Ct=/\+-|\.\.|\?\?\?\?|!!!!|,,|--/,yt=/\((c|tm|r)\)/i,Et=/\((c|tm|r)\)/gi,At={c:"\xa9",r:"\xae",tm:"\u2122"};function bt(t,e){return At[e.toLowerCase()]}function Ft(t){let e=0;for(let r=t.length-1;r>=0;r--){const n=t[r];"text"!==n.type||e||(n.content=n.content.replace(Et,bt)),"link_open"===n.type&&"auto"===n.info&&e--,"link_close"===n.type&&"auto"===n.info&&e++}}function xt(t){let e=0;for(let r=t.length-1;r>=0;r--){const n=t[r];"text"!==n.type||e||Ct.test(n.content)&&(n.content=n.content.replace(/\+-/g,"\xb1").replace(/\.{2,}/g,"\u2026").replace(/([?!])\u2026/g,"$1..").replace(/([?!]){4,}/g,"$1$1$1").replace(/,{2,}/g,",").replace(/(^|[^-])---(?=[^-]|$)/gm,"$1\u2014").replace(/(^|\s)--(?=\s|$)/gm,"$1\u2013").replace(/(^|[^-\s])--(?=[^-\s]|$)/gm,"$1\u2013")),"link_open"===n.type&&"auto"===n.info&&e--,"link_close"===n.type&&"auto"===n.info&&e++}}const wt=/['"]/,vt=/['"]/g,zt="\u2019";function St(t,e,r){return t.slice(0,e)+r+t.slice(e+1)}function qt(t,e){let r;const n=[];for(let s=0;s=0&&!(n[r].level<=o);r--);if(n.length=r+1,"text"!==i.type)continue;let u=i.content,c=0,a=u.length;t:for(;c=0)d=u.charCodeAt(l.index-1);else for(r=s-1;r>=0&&("softbreak"!==t[r].type&&"hardbreak"!==t[r].type);r--)if(t[r].content){d=t[r].content.charCodeAt(t[r].content.length-1);break}let _=32;if(c=48&&d<=57&&(p=h=!1),h&&p&&(h=m,p=g),h||p){if(p)for(r=n.length-1;r>=0;r--){let h=n[r];if(n[r].level=0;o--){const u=s[o];if("link_close"!==u.type){if("html_inline"===u.type&&(r=u.content,/^\s]/i.test(r)&&i>0&&i--,Dt(u.content)&&i++),!(i>0)&&"text"===u.type&&t.md.linkify.test(u.content)){const r=u.content;let i=t.md.linkify.match(r);const c=[];let a=u.level,l=0;i.length>0&&0===i[0].index&&o>0&&"text_special"===s[o-1].type&&(i=i.slice(1));for(let e=0;el){const e=new t.Token("text","",0);e.content=r.slice(l,u),e.level=a,c.push(e)}const h=new t.Token("link_open","a",1);h.attrs=[["href",s]],h.level=a++,h.markup="linkify",h.info="auto",c.push(h);const p=new t.Token("text","",0);p.content=o,p.level=a,c.push(p);const f=new t.Token("link_close","a",-1);f.level=--a,f.markup="linkify",f.info="auto",c.push(f),l=i[e].lastIndex}if(l=0;e--)"inline"===t.tokens[e].type&&(yt.test(t.tokens[e].content)&&Ft(t.tokens[e].children),Ct.test(t.tokens[e].content)&&xt(t.tokens[e].children))}],["smartquotes",function(t){if(t.md.options.typographer)for(let e=t.tokens.length-1;e>=0;e--)"inline"===t.tokens[e].type&&wt.test(t.tokens[e].content)&&qt(t.tokens[e].children,t)}],["text_join",function(t){let e,r;const n=t.tokens,s=n.length;for(let t=0;t0&&this.level++,this.tokens.push(n),n},It.prototype.isEmpty=function(t){return this.bMarks[t]+this.tShift[t]>=this.eMarks[t]},It.prototype.skipEmptyLines=function(t){for(let e=this.lineMax;te;)if(!st(this.src.charCodeAt(--t)))return t+1;return t},It.prototype.skipChars=function(t,e){for(let r=this.src.length;tr;)if(e!==this.src.charCodeAt(--t))return t+1;return t},It.prototype.getLines=function(t,e,r,n){if(t>=e)return"";const s=new Array(e-t);for(let i=0,o=t;or?new Array(t-r+1).join(" ")+this.src.slice(a,c):this.src.slice(a,c)}return s.join("")},It.prototype.Token=_t;function Mt(t,e){const r=t.bMarks[e]+t.tShift[e],n=t.eMarks[e];return t.src.slice(r,n)}function Tt(t){const e=[],r=t.length;let n=0,s=t.charCodeAt(n),i=!1,o=0,u="";for(;n=n)return-1;let i=t.src.charCodeAt(s++);if(i<48||i>57)return-1;for(;;){if(s>=n)return-1;if(i=t.src.charCodeAt(s++),!(i>=48&&i<=57)){if(41===i||46===i)break;return-1}if(s-r>=10)return-1}return s`\\x00-\\x20]+|'[^']*'|\"[^\"]*\"))?)*\\s*\\/?>",Ot="<\\/[A-Za-z][A-Za-z0-9\\-]*\\s*>",jt=new RegExp("^(?:"+Pt+"|"+Ot+"|\x3c!---?>|\x3c!--(?:[^-]|-[^-]|--[^>])*--\x3e|<[?][\\s\\S]*?[?]>|]*>|)"),Zt=new RegExp("^(?:"+Pt+"|"+Ot+")"),$t=[[/^<(script|pre|style|textarea)(?=(\s|>|$))/i,/<\/(script|pre|style|textarea)>/i,!0],[/^/,!0],[/^<\?/,/\?>/,!0],[/^/,!0],[/^/,!0],[new RegExp("^|$))","i"),/^$/,!0],[new RegExp(Zt.source+"\\s*$"),/^$/,!1]];const Ut=[["table",function(t,e,r,n){if(e+2>r)return!1;let s=e+1;if(t.sCount[s]=4)return!1;let i=t.bMarks[s]+t.tShift[s];if(i>=t.eMarks[s])return!1;const o=t.src.charCodeAt(i++);if(124!==o&&45!==o&&58!==o)return!1;if(i>=t.eMarks[s])return!1;const u=t.src.charCodeAt(i++);if(124!==u&&45!==u&&58!==u&&!st(u))return!1;if(45===o&&st(u))return!1;for(;i=4)return!1;a=Tt(c),a.length&&""===a[0]&&a.shift(),a.length&&""===a[a.length-1]&&a.pop();const h=a.length;if(0===h||h!==l.length)return!1;if(n)return!0;const p=t.parentType;t.parentType="table";const f=t.md.block.ruler.getRules("blockquote"),d=[e,0];t.push("table_open","table",1).map=d,t.push("thead_open","thead",1).map=[e,e+1],t.push("tr_open","tr",1).map=[e,e+1];for(let e=0;e=4)break;if(a=Tt(c),a.length&&""===a[0]&&a.shift(),a.length&&""===a[a.length-1]&&a.pop(),m+=h-a.length,m>65536)break;if(s===e+2){t.push("tbody_open","tbody",1).map=_=[e+2,0]}t.push("tr_open","tr",1).map=[s,s+1];for(let e=0;e=4))break;n++,s=n}t.line=s;const i=t.push("code_block","code",0);return i.content=t.getLines(e,s,4+t.blkIndent,!1)+"\n",i.map=[e,t.line],!0}],["fence",function(t,e,r,n){let s=t.bMarks[e]+t.tShift[e],i=t.eMarks[e];if(t.sCount[e]-t.blkIndent>=4)return!1;if(s+3>i)return!1;const o=t.src.charCodeAt(s);if(126!==o&&96!==o)return!1;let u=s;s=t.skipChars(s,o);let c=s-u;if(c<3)return!1;const a=t.src.slice(u,s),l=t.src.slice(s,i);if(96===o&&l.indexOf(String.fromCharCode(o))>=0)return!1;if(n)return!0;let h=e,p=!1;for(;(h++,!(h>=r))&&(s=u=t.bMarks[h]+t.tShift[h],i=t.eMarks[h],!(s=4||(s=t.skipChars(s,o),s-u=4)return!1;if(62!==t.src.charCodeAt(s))return!1;if(n)return!0;const u=[],c=[],a=[],l=[],h=t.md.block.ruler.getRules("blockquote"),p=t.parentType;t.parentType="blockquote";let f,d=!1;for(f=e;f=i)break;if(62===t.src.charCodeAt(s++)&&!e){let e,r,n=t.sCount[f]+1;32===t.src.charCodeAt(s)?(s++,n++,r=!1,e=!0):9===t.src.charCodeAt(s)?(e=!0,(t.bsCount[f]+n)%4==3?(s++,n++,r=!1):r=!0):e=!1;let o=n;for(u.push(t.bMarks[f]),t.bMarks[f]=s;s=i,c.push(t.bsCount[f]),t.bsCount[f]=t.sCount[f]+1+(e?1:0),a.push(t.sCount[f]),t.sCount[f]=o-n,l.push(t.tShift[f]),t.tShift[f]=s-t.bMarks[f];continue}if(d)break;let n=!1;for(let e=0,s=h.length;e";const g=[e,0];m.map=g,t.md.block.tokenize(t,e,f),t.push("blockquote_close","blockquote",-1).markup=">",t.lineMax=o,t.parentType=p,g[1]=t.line;for(let r=0;r=4)return!1;let i=t.bMarks[e]+t.tShift[e];const o=t.src.charCodeAt(i++);if(42!==o&&45!==o&&95!==o)return!1;let u=1;for(;i=4)return!1;if(t.listIndent>=0&&t.sCount[c]-t.listIndent>=4&&t.sCount[c]=t.blkIndent&&(f=!0),(p=Nt(t,c))>=0){if(l=!0,o=t.bMarks[c]+t.tShift[c],h=Number(t.src.slice(o,p-1)),f&&1!==h)return!1}else{if(!((p=Rt(t,c))>=0))return!1;l=!1}if(f&&t.skipSpaces(p)>=t.eMarks[c])return!1;if(n)return!0;const d=t.src.charCodeAt(p-1),_=t.tokens.length;l?(u=t.push("ordered_list_open","ol",1),1!==h&&(u.attrs=[["start",h]])):u=t.push("bullet_list_open","ul",1);const m=[c,0];u.map=m,u.markup=String.fromCharCode(d);let g=!1;const k=t.md.block.ruler.getRules("list"),D=t.parentType;for(t.parentType="list";c=s?1:n-e,f>4&&(f=1);const _=e+f;u=t.push("list_item_open","li",1),u.markup=String.fromCharCode(d);const m=[c,0];u.map=m,l&&(u.info=t.src.slice(o,p-1));const D=t.tight,C=t.tShift[c],y=t.sCount[c],E=t.listIndent;if(t.listIndent=t.blkIndent,t.blkIndent=_,t.tight=!0,t.tShift[c]=h-t.bMarks[c],t.sCount[c]=n,h>=s&&t.isEmpty(c+1)?t.line=Math.min(t.line+2,r):t.md.block.tokenize(t,c,r,!0),t.tight&&!g||(a=!1),g=t.line-c>1&&t.isEmpty(t.line-1),t.blkIndent=t.listIndent,t.listIndent=E,t.tShift[c]=C,t.sCount[c]=y,t.tight=D,u=t.push("list_item_close","li",-1),u.markup=String.fromCharCode(d),c=t.line,m[1]=c,c>=r)break;if(t.sCount[c]=4)break;let A=!1;for(let e=0,n=k.length;e=4)return!1;if(91!==t.src.charCodeAt(s))return!1;function u(e){const r=t.lineMax;if(e>=r||t.isEmpty(e))return null;let n=!1;if(t.sCount[e]-t.blkIndent>3&&(n=!0),t.sCount[e]<0&&(n=!0),!n){const n=t.md.block.ruler.getRules("reference"),s=t.parentType;t.parentType="reference";let i=!1;for(let s=0,o=n.length;s=4)return!1;if(!t.md.options.html)return!1;if(60!==t.src.charCodeAt(s))return!1;let o=t.src.slice(s,i),u=0;for(;u<$t.length&&!$t[u][0].test(o);u++);if(u===$t.length)return!1;if(n)return $t[u][2];let c=e+1;if(!$t[u][1].test(o))for(;c=4)return!1;let o=t.src.charCodeAt(s);if(35!==o||s>=i)return!1;let u=1;for(o=t.src.charCodeAt(++s);35===o&&s6||ss&&st(t.src.charCodeAt(c-1))&&(i=c),t.line=e+1;const a=t.push("heading_open","h"+String(u),1);a.markup="########".slice(0,u),a.map=[e,t.line];const l=t.push("inline","",0);return l.content=t.src.slice(s,i).trim(),l.map=[e,t.line],l.children=[],t.push("heading_close","h"+String(u),-1).markup="########".slice(0,u),!0},["paragraph","reference","blockquote"]],["lheading",function(t,e,r){const n=t.md.block.ruler.getRules("paragraph");if(t.sCount[e]-t.blkIndent>=4)return!1;const s=t.parentType;t.parentType="paragraph";let i,o=0,u=e+1;for(;u3)continue;if(t.sCount[u]>=t.blkIndent){let e=t.bMarks[u]+t.tShift[u];const r=t.eMarks[u];if(e=r))){o=61===i?1:2;break}}if(t.sCount[u]<0)continue;let e=!1;for(let s=0,i=n.length;s3)continue;if(t.sCount[i]<0)continue;let e=!1;for(let s=0,o=n.length;s=r))&&!(t.sCount[o]=i){t.line=r;break}const e=t.line;let c=!1;for(let i=0;i=t.line)throw new Error("block rule didn't increment state.line");break}if(!c)throw new Error("none of the block rules matched");t.tight=!u,t.isEmpty(t.line-1)&&(u=!0),o=t.line,o0&&(this.level++,this._prev_delimiters.push(this.delimiters),this.delimiters=[],s={delimiters:this.delimiters}),this.pendingLevel=this.level,this.tokens.push(n),this.tokens_meta.push(s),n},Vt.prototype.scanDelims=function(t,e){const r=this.posMax,n=this.src.charCodeAt(t),s=t>0?this.src.charCodeAt(t-1):32;let i=t;for(;i?@[]^_`{|}~-".split("").forEach((function(t){Jt[t.charCodeAt(0)]=1}));var Xt={tokenize:function(t,e){const r=t.pos,n=t.src.charCodeAt(r);if(e)return!1;if(126!==n)return!1;const s=t.scanDelims(t.pos,!0);let i=s.length;const o=String.fromCharCode(n);if(i<2)return!1;let u;i%2&&(u=t.push("text","",0),u.content=o,i--);for(let e=0;e=0;r--){const n=e[r];if(95!==n.marker&&42!==n.marker)continue;if(-1===n.end)continue;const s=e[n.end],i=r>0&&e[r-1].end===n.end+1&&e[r-1].marker===n.marker&&e[r-1].token===n.token-1&&e[n.end+1].token===s.token+1,o=String.fromCharCode(n.marker),u=t.tokens[n.token];u.type=i?"strong_open":"em_open",u.tag=i?"strong":"em",u.nesting=1,u.markup=i?o+o:o,u.content="";const c=t.tokens[s.token];c.type=i?"strong_close":"em_close",c.tag=i?"strong":"em",c.nesting=-1,c.markup=i?o+o:o,c.content="",i&&(t.tokens[e[r-1].token].content="",t.tokens[e[n.end+1].token].content="",r--)}}var Kt={tokenize:function(t,e){const r=t.pos,n=t.src.charCodeAt(r);if(e)return!1;if(95!==n&&42!==n)return!1;const s=t.scanDelims(t.pos,42===n);for(let e=0;e\x00-\x20]*)$/;const re=/^&#((?:x[a-f0-9]{1,6}|[0-9]{1,7}));/i,ne=/^&([a-z][a-z0-9]{1,31});/i;function se(t){const e={},r=t.length;if(!r)return;let n=0,s=-2;const i=[];for(let o=0;ou;c-=i[c]+1){const e=t[c];if(e.marker===r.marker&&(e.open&&e.end<0)){let n=!1;if((e.close||r.open)&&(e.length+r.length)%3==0&&(e.length%3==0&&r.length%3==0||(n=!0)),!n){const n=c>0&&!t[c-1].open?i[c-1]+1:0;i[o]=o-c+n,i[c]=n,r.open=!1,e.end=o,e.close=!1,a=-1,s=-2;break}}}-1!==a&&(e[r.marker][(r.open?3:0)+(r.length||0)%3]=a)}}const ie=[["text",function(t,e){let r=t.pos;for(;r0)return!1;const r=t.pos;if(r+3>t.posMax)return!1;if(58!==t.src.charCodeAt(r))return!1;if(47!==t.src.charCodeAt(r+1))return!1;if(47!==t.src.charCodeAt(r+2))return!1;const n=t.pending.match(Wt);if(!n)return!1;const s=n[1],i=t.md.linkify.matchAtStart(t.src.slice(r-s.length));if(!i)return!1;let o=i.url;if(o.length<=s.length)return!1;o=o.replace(/\*+$/,"");const u=t.md.normalizeLink(o);if(!t.md.validateLink(u))return!1;if(!e){t.pending=t.pending.slice(0,-s.length);const e=t.push("link_open","a",1);e.attrs=[["href",u]],e.markup="linkify",e.info="auto";t.push("text","",0).content=t.md.normalizeLinkText(o);const r=t.push("link_close","a",-1);r.markup="linkify",r.info="auto"}return t.pos+=o.length-s.length,!0}],["newline",function(t,e){let r=t.pos;if(10!==t.src.charCodeAt(r))return!1;const n=t.pending.length-1,s=t.posMax;if(!e)if(n>=0&&32===t.pending.charCodeAt(n))if(n>=1&&32===t.pending.charCodeAt(n-1)){let e=n-1;for(;e>=1&&32===t.pending.charCodeAt(e-1);)e--;t.pending=t.pending.slice(0,e),t.push("hardbreak","br",0)}else t.pending=t.pending.slice(0,-1),t.push("softbreak","br",0);else t.push("softbreak","br",0);for(r++;r=n)return!1;let s=t.src.charCodeAt(r);if(10===s){for(e||t.push("hardbreak","br",0),r++;r=55296&&s<=56319&&r+1=56320&&e<=57343&&(i+=t.src[r+1],r++)}const o="\\"+i;if(!e){const e=t.push("text_special","",0);s<256&&0!==Jt[s]?e.content=i:e.content=o,e.markup=o,e.info="escape"}return t.pos=r+1,!0}],["backticks",function(t,e){let r=t.pos;if(96!==t.src.charCodeAt(r))return!1;const n=r;r++;const s=t.posMax;for(;r=h)return!1;if(c=d,s=t.md.helpers.parseLinkDestination(t.src,d,t.posMax),s.ok){for(o=t.md.normalizeLink(s.str),t.md.validateLink(o)?d=s.pos:o="",c=d;d=h||41!==t.src.charCodeAt(d))&&(a=!0),d++}if(a){if(void 0===t.env.references)return!1;if(d=0?n=t.src.slice(c,d++):d=f+1):d=f+1,n||(n=t.src.slice(p,f)),i=t.env.references[ct(n)],!i)return t.pos=l,!1;o=i.href,u=i.title}if(!e){t.pos=p,t.posMax=f;const e=[["href",o]];t.push("link_open","a",1).attrs=e,u&&e.push(["title",u]),t.linkLevel++,t.md.inline.tokenize(t),t.linkLevel--,t.push("link_close","a",-1)}return t.pos=d,t.posMax=h,!0}],["image",function(t,e){let r,n,s,i,o,u,c,a,l="";const h=t.pos,p=t.posMax;if(33!==t.src.charCodeAt(t.pos))return!1;if(91!==t.src.charCodeAt(t.pos+1))return!1;const f=t.pos+2,d=t.md.helpers.parseLinkLabel(t,t.pos+1,!1);if(d<0)return!1;if(i=d+1,i=p)return!1;for(a=i,u=t.md.helpers.parseLinkDestination(t.src,i,t.posMax),u.ok&&(l=t.md.normalizeLink(u.str),t.md.validateLink(l)?i=u.pos:l=""),a=i;i=p||41!==t.src.charCodeAt(i))return t.pos=h,!1;i++}else{if(void 0===t.env.references)return!1;if(i=0?s=t.src.slice(a,i++):i=d+1):i=d+1,s||(s=t.src.slice(f,d)),o=t.env.references[ct(s)],!o)return t.pos=h,!1;l=o.href,c=o.title}if(!e){n=t.src.slice(f,d);const e=[];t.md.inline.parse(n,t.md,t.env,e);const r=t.push("image","img",0),s=[["src",l],["alt",""]];r.attrs=s,r.children=e,r.content=n,c&&s.push(["title",c])}return t.pos=i,t.posMax=p,!0}],["autolink",function(t,e){let r=t.pos;if(60!==t.src.charCodeAt(r))return!1;const n=t.pos,s=t.posMax;for(;;){if(++r>=s)return!1;const e=t.src.charCodeAt(r);if(60===e)return!1;if(62===e)break}const i=t.src.slice(n+1,r);if(ee.test(i)){const r=t.md.normalizeLink(i);if(!t.md.validateLink(r))return!1;if(!e){const e=t.push("link_open","a",1);e.attrs=[["href",r]],e.markup="autolink",e.info="auto";t.push("text","",0).content=t.md.normalizeLinkText(i);const n=t.push("link_close","a",-1);n.markup="autolink",n.info="auto"}return t.pos+=i.length+2,!0}if(te.test(i)){const r=t.md.normalizeLink("mailto:"+i);if(!t.md.validateLink(r))return!1;if(!e){const e=t.push("link_open","a",1);e.attrs=[["href",r]],e.markup="autolink",e.info="auto";t.push("text","",0).content=t.md.normalizeLinkText(i);const n=t.push("link_close","a",-1);n.markup="autolink",n.info="auto"}return t.pos+=i.length+2,!0}return!1}],["html_inline",function(t,e){if(!t.md.options.html)return!1;const r=t.posMax,n=t.pos;if(60!==t.src.charCodeAt(n)||n+2>=r)return!1;const s=t.src.charCodeAt(n+1);if(33!==s&&63!==s&&47!==s&&!function(t){const e=32|t;return e>=97&&e<=122}(s))return!1;const i=t.src.slice(n).match(jt);if(!i)return!1;if(!e){const e=t.push("html_inline","",0);e.content=i[0],o=e.content,/^\s]/i.test(o)&&t.linkLevel++,function(t){return/^<\/a\s*>/i.test(t)}(e.content)&&t.linkLevel--}var o;return t.pos+=i[0].length,!0}],["entity",function(t,e){const r=t.pos,n=t.posMax;if(38!==t.src.charCodeAt(r))return!1;if(r+1>=n)return!1;if(35===t.src.charCodeAt(r+1)){const n=t.src.slice(r).match(re);if(n){if(!e){const e="x"===n[1][0].toLowerCase()?parseInt(n[1].slice(1),16):parseInt(n[1],10),r=t.push("text_special","",0);r.content=V(e)?G(e):G(65533),r.markup=n[0],r.info="entity"}return t.pos+=n[0].length,!0}}else{const n=t.src.slice(r).match(ne);if(n){const r=j(n[0]);if(r!==n[0]){if(!e){const e=t.push("text_special","",0);e.content=r,e.markup=n[0],e.info="entity"}return t.pos+=n[0].length,!0}}}return!1}]],oe=[["balance_pairs",function(t){const e=t.tokens_meta,r=t.tokens_meta.length;se(t.delimiters);for(let t=0;t0&&n++,"text"===s[e].type&&e+1=t.pos)throw new Error("inline rule didn't increment state.pos");break}}else t.pos=t.posMax;o||t.pos++,i[e]=t.pos},ue.prototype.tokenize=function(t){const e=this.ruler.getRules(""),r=e.length,n=t.posMax,s=t.md.options.maxNesting;for(;t.pos=t.pos)throw new Error("inline rule didn't increment state.pos");break}if(o){if(t.pos>=n)break}else t.pending+=t.src[t.pos++]}t.pending&&t.pushPending()},ue.prototype.parse=function(t,e,r,n){const s=new this.State(t,e,r,n);this.tokenize(s);const i=this.ruler2.getRules(""),o=i.length;for(let t=0;t=3&&":"===t[e-3]||e>=3&&"/"===t[e-3]?0:n.match(r.re.no_http)[0].length:0}},"mailto:":{validate:function(t,e,r){const n=t.slice(e);return r.re.mailto||(r.re.mailto=new RegExp("^"+r.re.src_email_name+"@"+r.re.src_host_strict,"i")),r.re.mailto.test(n)?n.match(r.re.mailto)[0].length:0}}},de="a[cdefgilmnoqrstuwxz]|b[abdefghijmnorstvwyz]|c[acdfghiklmnoruvwxyz]|d[ejkmoz]|e[cegrstu]|f[ijkmor]|g[abdefghilmnpqrstuwy]|h[kmnrtu]|i[delmnoqrst]|j[emop]|k[eghimnprwyz]|l[abcikrstuvy]|m[acdeghklmnopqrstuvwxyz]|n[acefgilopruz]|om|p[aefghklmnrstwy]|qa|r[eosuw]|s[abcdeghijklmnortuvxyz]|t[cdfghjklmnortvwz]|u[agksyz]|v[aceginu]|w[fs]|y[et]|z[amw]",_e="biz|com|edu|gov|net|org|pro|web|xxx|aero|asia|coop|info|museum|name|shop|\u0440\u0444".split("|");function me(t){const e=t.re=function(t){const e={};t=t||{},e.src_Any=C.source,e.src_Cc=y.source,e.src_Z=b.source,e.src_P=E.source,e.src_ZPCc=[e.src_Z,e.src_P,e.src_Cc].join("|"),e.src_ZCc=[e.src_Z,e.src_Cc].join("|");const r="[><\uff5c]";return e.src_pseudo_letter="(?:(?![><\uff5c]|"+e.src_ZPCc+")"+e.src_Any+")",e.src_ip4="(?:(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)",e.src_auth="(?:(?:(?!"+e.src_ZCc+"|[@/\\[\\]()]).)+@)?",e.src_port="(?::(?:6(?:[0-4]\\d{3}|5(?:[0-4]\\d{2}|5(?:[0-2]\\d|3[0-5])))|[1-5]?\\d{1,4}))?",e.src_host_terminator="(?=$|[><\uff5c]|"+e.src_ZPCc+")(?!"+(t["---"]?"-(?!--)|":"-|")+"_|:\\d|\\.-|\\.(?!$|"+e.src_ZPCc+"))",e.src_path="(?:[/?#](?:(?!"+e.src_ZCc+"|"+r+"|[()[\\]{}.,\"'?!\\-;]).|\\[(?:(?!"+e.src_ZCc+"|\\]).)*\\]|\\((?:(?!"+e.src_ZCc+"|[)]).)*\\)|\\{(?:(?!"+e.src_ZCc+'|[}]).)*\\}|\\"(?:(?!'+e.src_ZCc+'|["]).)+\\"|\\\'(?:(?!'+e.src_ZCc+"|[']).)+\\'|\\'(?="+e.src_pseudo_letter+"|[-])|\\.{2,}[a-zA-Z0-9%/&]|\\.(?!"+e.src_ZCc+"|[.]|$)|"+(t["---"]?"\\-(?!--(?:[^-]|$))(?:-*)|":"\\-+|")+",(?!"+e.src_ZCc+"|$)|;(?!"+e.src_ZCc+"|$)|\\!+(?!"+e.src_ZCc+"|[!]|$)|\\?(?!"+e.src_ZCc+"|[?]|$))+|\\/)?",e.src_email_name='[\\-;:&=\\+\\$,\\.a-zA-Z0-9_][\\-;:&=\\+\\$,\\"\\.a-zA-Z0-9_]*',e.src_xn="xn--[a-z0-9\\-]{1,59}",e.src_domain_root="(?:"+e.src_xn+"|"+e.src_pseudo_letter+"{1,63})",e.src_domain="(?:"+e.src_xn+"|(?:"+e.src_pseudo_letter+")|(?:"+e.src_pseudo_letter+"(?:-|"+e.src_pseudo_letter+"){0,61}"+e.src_pseudo_letter+"))",e.src_host="(?:(?:(?:(?:"+e.src_domain+")\\.)*"+e.src_domain+"))",e.tpl_host_fuzzy="(?:"+e.src_ip4+"|(?:(?:(?:"+e.src_domain+")\\.)+(?:%TLDS%)))",e.tpl_host_no_ip_fuzzy="(?:(?:(?:"+e.src_domain+")\\.)+(?:%TLDS%))",e.src_host_strict=e.src_host+e.src_host_terminator,e.tpl_host_fuzzy_strict=e.tpl_host_fuzzy+e.src_host_terminator,e.src_host_port_strict=e.src_host+e.src_port+e.src_host_terminator,e.tpl_host_port_fuzzy_strict=e.tpl_host_fuzzy+e.src_port+e.src_host_terminator,e.tpl_host_port_no_ip_fuzzy_strict=e.tpl_host_no_ip_fuzzy+e.src_port+e.src_host_terminator,e.tpl_host_fuzzy_test="localhost|www\\.|\\.\\d{1,3}\\.|(?:\\.(?:%TLDS%)(?:"+e.src_ZPCc+"|>|$))",e.tpl_email_fuzzy='(^|[><\uff5c]|"|\\(|'+e.src_ZCc+")("+e.src_email_name+"@"+e.tpl_host_fuzzy_strict+")",e.tpl_link_fuzzy="(^|(?![.:/\\-_@])(?:[$+<=>^`|\uff5c]|"+e.src_ZPCc+"))((?![$+<=>^`|\uff5c])"+e.tpl_host_port_fuzzy_strict+e.src_path+")",e.tpl_link_no_ip_fuzzy="(^|(?![.:/\\-_@])(?:[$+<=>^`|\uff5c]|"+e.src_ZPCc+"))((?![$+<=>^`|\uff5c])"+e.tpl_host_port_no_ip_fuzzy_strict+e.src_path+")",e}(t.__opts__),r=t.__tlds__.slice();function n(t){return t.replace("%TLDS%",e.src_tlds)}t.onCompile(),t.__tlds_replaced__||r.push(de),r.push(e.src_xn),e.src_tlds=r.join("|"),e.email_fuzzy=RegExp(n(e.tpl_email_fuzzy),"i"),e.link_fuzzy=RegExp(n(e.tpl_link_fuzzy),"i"),e.link_no_ip_fuzzy=RegExp(n(e.tpl_link_no_ip_fuzzy),"i"),e.host_fuzzy_test=RegExp(n(e.tpl_host_fuzzy_test),"i");const s=[];function i(t,e){throw new Error('(LinkifyIt) Invalid schema "'+t+'": '+e)}t.__compiled__={},Object.keys(t.__schemas__).forEach((function(e){const r=t.__schemas__[e];if(null===r)return;const n={validate:null,link:null};if(t.__compiled__[e]=n,"[object Object]"===ae(r))return!function(t){return"[object RegExp]"===ae(t)}(r.validate)?le(r.validate)?n.validate=r.validate:i(e,r):n.validate=function(t){return function(e,r){const n=e.slice(r);return t.test(n)?n.match(t)[0].length:0}}(r.validate),void(le(r.normalize)?n.normalize=r.normalize:r.normalize?i(e,r):n.normalize=function(t,e){e.normalize(t)});!function(t){return"[object String]"===ae(t)}(r)?i(e,r):s.push(e)})),s.forEach((function(e){t.__compiled__[t.__schemas__[e]]&&(t.__compiled__[e].validate=t.__compiled__[t.__schemas__[e]].validate,t.__compiled__[e].normalize=t.__compiled__[t.__schemas__[e]].normalize)})),t.__compiled__[""]={validate:null,normalize:function(t,e){e.normalize(t)}};const o=Object.keys(t.__compiled__).filter((function(e){return e.length>0&&t.__compiled__[e]})).map(he).join("|");t.re.schema_test=RegExp("(^|(?!_)(?:[><\uff5c]|"+e.src_ZPCc+"))("+o+")","i"),t.re.schema_search=RegExp("(^|(?!_)(?:[><\uff5c]|"+e.src_ZPCc+"))("+o+")","ig"),t.re.schema_at_start=RegExp("^"+t.re.schema_search.source,"i"),t.re.pretest=RegExp("("+t.re.schema_test.source+")|("+t.re.host_fuzzy_test.source+")|@","i"),function(t){t.__index__=-1,t.__text_cache__=""}(t)}function ge(t,e){const r=t.__index__,n=t.__last_index__,s=t.__text_cache__.slice(r,n);this.schema=t.__schema__.toLowerCase(),this.index=r+e,this.lastIndex=n+e,this.raw=s,this.text=s,this.url=s}function ke(t,e){const r=new ge(t,e);return t.__compiled__[r.schema].normalize(r,t),r}function De(t,e){if(!(this instanceof De))return new De(t,e);var r;e||(r=t,Object.keys(r||{}).reduce((function(t,e){return t||pe.hasOwnProperty(e)}),!1)&&(e=t,t={})),this.__opts__=ce({},pe,e),this.__index__=-1,this.__last_index__=-1,this.__schema__="",this.__text_cache__="",this.__schemas__=ce({},fe,t),this.__compiled__={},this.__tlds__=_e,this.__tlds_replaced__=!1,this.re={},me(this)}De.prototype.add=function(t,e){return this.__schemas__[t]=e,me(this),this},De.prototype.set=function(t){return this.__opts__=ce(this.__opts__,t),this},De.prototype.test=function(t){if(this.__text_cache__=t,this.__index__=-1,!t.length)return!1;let e,r,n,s,i,o,u,c,a;if(this.re.schema_test.test(t))for(u=this.re.schema_search,u.lastIndex=0;null!==(e=u.exec(t));)if(s=this.testSchemaAt(t,e[2],u.lastIndex),s){this.__schema__=e[2],this.__index__=e.index+e[1].length,this.__last_index__=e.index+e[0].length+s;break}return this.__opts__.fuzzyLink&&this.__compiled__["http:"]&&(c=t.search(this.re.host_fuzzy_test),c>=0&&(this.__index__<0||c=0&&null!==(n=t.match(this.re.email_fuzzy))&&(i=n.index+n[1].length,o=n.index+n[0].length,(this.__index__<0||ithis.__last_index__)&&(this.__schema__="mailto:",this.__index__=i,this.__last_index__=o))),this.__index__>=0},De.prototype.pretest=function(t){return this.re.pretest.test(t)},De.prototype.testSchemaAt=function(t,e,r){return this.__compiled__[e.toLowerCase()]?this.__compiled__[e.toLowerCase()].validate(t,r,this):0},De.prototype.match=function(t){const e=[];let r=0;this.__index__>=0&&this.__text_cache__===t&&(e.push(ke(this,r)),r=this.__last_index__);let n=r?t.slice(r):t;for(;this.test(n);)e.push(ke(this,r)),n=n.slice(this.__last_index__),r+=this.__last_index__;return e.length?e:null},De.prototype.matchAtStart=function(t){if(this.__text_cache__=t,this.__index__=-1,!t.length)return null;const e=this.re.schema_at_start.exec(t);if(!e)return null;const r=this.testSchemaAt(t,e[2],e[0].length);return r?(this.__schema__=e[2],this.__index__=e.index+e[1].length,this.__last_index__=e.index+e[0].length+r,ke(this,0)):null},De.prototype.tlds=function(t,e){return t=Array.isArray(t)?t:[t],e?(this.__tlds__=this.__tlds__.concat(t).sort().filter((function(t,e,r){return t!==r[e-1]})).reverse(),me(this),this):(this.__tlds__=t.slice(),this.__tlds_replaced__=!0,me(this),this)},De.prototype.normalize=function(t){t.schema||(t.url="http://"+t.url),"mailto:"!==t.schema||/^mailto:/i.test(t.url)||(t.url="mailto:"+t.url)},De.prototype.onCompile=function(){};const Ce=2147483647,ye=36,Ee=/^xn--/,Ae=/[^\0-\x7F]/,be=/[\x2E\u3002\uFF0E\uFF61]/g,Fe={overflow:"Overflow: input needs wider integers to process","not-basic":"Illegal input >= 0x80 (not a basic code point)","invalid-input":"Invalid input"},xe=Math.floor,we=String.fromCharCode;function ve(t){throw new RangeError(Fe[t])}function ze(t,e){const r=t.split("@");let n="";r.length>1&&(n=r[0]+"@",t=r[1]);const s=function(t,e){const r=[];let n=t.length;for(;n--;)r[n]=e(t[n]);return r}((t=t.replace(be,".")).split("."),e).join(".");return n+s}function Se(t){const e=[];let r=0;const n=t.length;for(;r=55296&&s<=56319&&r>1,t+=xe(t/e);t>455;n+=ye)t=xe(t/35);return xe(n+36*t/(t+38))},Le=function(t){const e=[],r=t.length;let n=0,s=128,i=72,o=t.lastIndexOf("-");o<0&&(o=0);for(let r=0;r=128&&ve("not-basic"),e.push(t.charCodeAt(r));for(let c=o>0?o+1:0;c=r&&ve("invalid-input");const o=(u=t.charCodeAt(c++))>=48&&u<58?u-48+26:u>=65&&u<91?u-65:u>=97&&u<123?u-97:ye;o>=ye&&ve("invalid-input"),o>xe((Ce-n)/e)&&ve("overflow"),n+=o*e;const a=s<=i?1:s>=i+26?26:s-i;if(oxe(Ce/l)&&ve("overflow"),e*=l}const a=e.length+1;i=Be(n-o,a,0==o),xe(n/a)>Ce-s&&ve("overflow"),s+=xe(n/a),n%=a,e.splice(n++,0,s)}var u;return String.fromCodePoint(...e)},Ie=function(t){const e=[],r=(t=Se(t)).length;let n=128,s=0,i=72;for(const r of t)r<128&&e.push(we(r));const o=e.length;let u=o;for(o&&e.push("-");u=n&&exe((Ce-s)/c)&&ve("overflow"),s+=(r-n)*c,n=r;for(const r of t)if(rCe&&ve("overflow"),r===n){let t=s;for(let r=ye;;r+=ye){const n=r<=i?1:r>=i+26?26:r-i;if(tString.fromCodePoint(...t)},decode:Le,encode:Ie,toASCII:function(t){return ze(t,(function(t){return Ae.test(t)?"xn--"+Ie(t):t}))},toUnicode:function(t){return ze(t,(function(t){return Ee.test(t)?Le(t.slice(4).toLowerCase()):t}))}};const Te={default:{options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"\u201c\u201d\u2018\u2019",highlight:null,maxNesting:100},components:{core:{},block:{},inline:{}}},zero:{options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"\u201c\u201d\u2018\u2019",highlight:null,maxNesting:20},components:{core:{rules:["normalize","block","inline","text_join"]},block:{rules:["paragraph"]},inline:{rules:["text"],rules2:["balance_pairs","fragments_join"]}}},commonmark:{options:{html:!0,xhtmlOut:!0,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"\u201c\u201d\u2018\u2019",highlight:null,maxNesting:20},components:{core:{rules:["normalize","block","inline","text_join"]},block:{rules:["blockquote","code","fence","heading","hr","html_block","lheading","list","reference","paragraph"]},inline:{rules:["autolink","backticks","emphasis","entity","escape","html_inline","image","link","newline","text"],rules2:["balance_pairs","emphasis","fragments_join"]}}}},Re=/^(vbscript|javascript|file|data):/,Ne=/^data:image\/(gif|png|jpeg|webp);/;function Pe(t){const e=t.trim().toLowerCase();return!Re.test(e)||Ne.test(e)}const Oe=["http:","https:","mailto:"];function je(t){const e=g(t,!0);if(e.hostname&&(!e.protocol||Oe.indexOf(e.protocol)>=0))try{e.hostname=Me.toASCII(e.hostname)}catch(t){}return n(s(e))}function Ze(t){const r=g(t,!0);if(r.hostname&&(!r.protocol||Oe.indexOf(r.protocol)>=0))try{r.hostname=Me.toUnicode(r.hostname)}catch(t){}return e(s(r),e.defaultChars+"%")}function $e(t,e){if(!(this instanceof $e))return new $e(t,e);e||Z(t)||(e=t||{},t="default"),this.inline=new ue,this.block=new Ht,this.core=new Lt,this.renderer=new ft,this.linkify=new De,this.validateLink=Pe,this.normalizeLink=je,this.normalizeLinkText=Ze,this.utils=lt,this.helpers=U({},ht),this.options={},this.configure(t),e&&this.set(e)}return $e.prototype.set=function(t){return U(this.options,t),this},$e.prototype.configure=function(t){const e=this;if(Z(t)){const e=t;if(!(t=Te[e]))throw new Error('Wrong `markdown-it` preset "'+e+'", check name')}if(!t)throw new Error("Wrong `markdown-it` preset, can't be empty");return t.options&&e.set(t.options),t.components&&Object.keys(t.components).forEach((function(r){t.components[r].rules&&e[r].ruler.enableOnly(t.components[r].rules),t.components[r].rules2&&e[r].ruler2.enableOnly(t.components[r].rules2)})),this},$e.prototype.enable=function(t,e){let r=[];Array.isArray(t)||(t=[t]),["core","block","inline"].forEach((function(e){r=r.concat(this[e].ruler.enable(t,!0))}),this),r=r.concat(this.inline.ruler2.enable(t,!0));const n=t.filter((function(t){return r.indexOf(t)<0}));if(n.length&&!e)throw new Error("MarkdownIt. Failed to enable unknown rule(s): "+n);return this},$e.prototype.disable=function(t,e){let r=[];Array.isArray(t)||(t=[t]),["core","block","inline"].forEach((function(e){r=r.concat(this[e].ruler.disable(t,!0))}),this),r=r.concat(this.inline.ruler2.disable(t,!0));const n=t.filter((function(t){return r.indexOf(t)<0}));if(n.length&&!e)throw new Error("MarkdownIt. Failed to disable unknown rule(s): "+n);return this},$e.prototype.use=function(t){const e=[this].concat(Array.prototype.slice.call(arguments,1));return t.apply(t,e),this},$e.prototype.parse=function(t,e){if("string"!=typeof t)throw new Error("Input data should be a String");const r=new this.core.State(t,this,e);return this.core.process(r),r.tokens},$e.prototype.render=function(t,e){return e=e||{},this.renderer.render(this.parse(t,e),this.options,e)},$e.prototype.parseInline=function(t,e){const r=new this.core.State(t,this,e);return r.inlineMode=!0,this.core.process(r),r.tokens},$e.prototype.renderInline=function(t,e){return e=e||{},this.renderer.render(this.parseInline(t,e),this.options,e)},$e})); diff --git a/plugins/voyage/playground/vendor/playground-design-system/CHANGELOG.md b/plugins/voyage/playground/vendor/playground-design-system/CHANGELOG.md deleted file mode 100644 index 1594aa0..0000000 --- a/plugins/voyage/playground/vendor/playground-design-system/CHANGELOG.md +++ /dev/null @@ -1,98 +0,0 @@ -# playground-design-system — CHANGELOG - -## 0.5.0 — 2026-05-10 - -### Added -- **voyage scope tokens (B-DS-4):** `--color-scope-voyage` (aqua-blue `#1B5FB8`), `--color-scope-voyage-soft` (`#E5EFFA`), `--color-scope-voyage-strong` (`#143E78`) appended to scope-color group in `tokens.css`. Matches the existing `--color-scope-{architect,okr,security,ultraplan,config}` family so voyage-playground can use the canonical badge convention. -- **`.badge--scope-voyage`** in `base.css`: white-on-aqua-blue badge variant matching the existing scope-badge family. - -### Påvirkning - -Endringen er **additiv**: legger TIL voyage-scope-tokens og en ny badge-modifier. Ingen eksisterende selectors eller token-verdier endres. Plugin-konsumenter (llm-security, ms-ai-architect, okr, config-audit) får stale vendor-state mot ny source-commit, men det er silent drift — re-sync skjer på eget tempo neste playground-touch. Bare `voyage` re-syncer i denne commit-en. - -Førsteadopter: `voyage` v4.3.0 (multi-sesjons-løp 2026-05-10, sesjon 1 = Wave 0+1 Foundation). - -## 0.4.0 — 2026-05-08 - -### Bug fixes -- **`.kanban-card__name`** (components-tier3-supplement.css): bytt `word-break: break-all` til `word-break: break-word` + `overflow-wrap: anywhere`. `break-all` knekker midt i ord ("Tekn isk dokumen tasjon"); ny verdi respekterer ordskjøt og brytter kun lange tokens (B-DS-1). -- **`.expansion__title-main`, `.expansion__title-sub`** (components-tier3-supplement.css): legg til `display: block`. Begge er ``-elementer som flyter inline by default, noe som gir "dokumentertKilde: Art. 9" på samme linje. `display: block` sikrer vertikal stacking (B-DS-2). -- **`.matrix__bubble`** (components.css): legg til `cursor: pointer`, `transition`, `:hover { transform: scale(1.15) }` og `:focus-visible { outline + offset }`. Antar at consumer rendrer bobler som ` - - - - - -
-
-

Ingen artifact lastet enda. Lim inn innhold over og trykk «Render».

-
-
- - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - diff --git a/plugins/voyage/playwright.config.mjs b/plugins/voyage/playwright.config.mjs deleted file mode 100644 index b4c1fc3..0000000 --- a/plugins/voyage/playwright.config.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import { defineConfig, devices } from '@playwright/test'; - -export default defineConfig({ - testDir: 'tests/e2e', - testMatch: '**/*.spec.mjs', - snapshotPathTemplate: '{testDir}/snapshots/{arg}{ext}', - timeout: 30_000, - expect: { timeout: 5_000 }, - fullyParallel: false, - forbidOnly: !!process.env.CI, - retries: 0, - reporter: process.env.CI ? 'github' : 'list', - use: { - baseURL: `file://${import.meta.dirname}/playground/`, - trace: 'retain-on-failure', - }, - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - ], -}); diff --git a/plugins/voyage/scripts/render-artifact.mjs b/plugins/voyage/scripts/render-artifact.mjs index fcbed66..09b9287 100644 --- a/plugins/voyage/scripts/render-artifact.mjs +++ b/plugins/voyage/scripts/render-artifact.mjs @@ -1,101 +1,284 @@ #!/usr/bin/env node // scripts/render-artifact.mjs -// CLI renderer for v4.2 — satisfies brief SC1 + SC11 (zero-network, self-eat). +// +// Renders a voyage artifact (brief.md / plan.md / review.md) to a +// self-contained HTML file in the same directory, with inlined CSS and +// zero external network references. The producing commands (/trekbrief, +// /trekplan, /trekreview) call this at the end and print the file:// link +// so the operator can read the artifact in a browser — and, when they want +// to annotate it, run the official `/playground` plugin (document-critique +// template) on it and paste the generated prompt back into Claude Code. // // Usage: -// node scripts/render-artifact.mjs [--out ] +// node scripts/render-artifact.mjs [--out ] // -// Reads input.md, renders it via the same vendored markdown-it + -// markdown-it-front-matter + highlight.js bundle that the browser -// playground uses (playground/lib/*.min.js), and emits a self-contained -// HTML file with inlined CSS + inlined highlight.js so the output renders -// correctly with zero network requests. +// Determinism: no timestamps, no random IDs — two runs on the same input +// produce byte-identical output. // -// Determinism contract (SC11): two invocations on the same input produce -// byte-identical output. No timestamps, no random IDs. +// Zero npm deps (marketplace convention). The markdown→HTML conversion is a +// small hand-rolled subset that covers what the artifact templates emit: +// ATX headings, ordered/unordered/nested lists, fenced code blocks, inline +// code, bold, links, blockquotes, GitHub-style tables, and horizontal rules. import { readFileSync, writeFileSync, existsSync } from 'node:fs'; -import { dirname, basename, resolve, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { basename } from 'node:path'; +import { splitFrontmatter } from '../lib/util/frontmatter.mjs'; -const HERE = dirname(fileURLToPath(import.meta.url)); -const ROOT = resolve(HERE, '..'); -const PLAYGROUND_LIB = join(ROOT, 'playground', 'lib'); -const DS_DIR = join(ROOT, 'playground', 'vendor', 'playground-design-system'); - -// --- argument parsing ------------------------------------------------------- - -function parseArgs(argv) { - const args = { input: null, out: null }; - for (let i = 0; i < argv.length; i++) { - const a = argv[i]; - if (a === '--out') { - args.out = argv[++i]; - } else if (a === '--help' || a === '-h') { - args.help = true; - } else if (!args.input) { - args.input = a; - } - } - return args; -} - -// --- vendored-lib loader (CommonJS shim) ------------------------------------ - -function loadVendoredScript(name, globalName) { - const src = readFileSync(join(PLAYGROUND_LIB, name), 'utf-8'); - const sandbox = {}; - // Minimal browser-shim: provide window/globalThis aliases the IIFE bundles - // expect when running outside the browser. - const fn = new Function('window', 'globalThis', 'self', src); - fn(sandbox, sandbox, sandbox); - return sandbox[globalName]; -} - -// --- inline-asset loaders --------------------------------------------------- - -function readDsCss() { - const order = [ - 'tokens.css', - 'base.css', - 'fonts.css', - 'components.css', - 'components-tier2.css', - 'components-tier3.css', - 'components-tier3-supplement.css', - 'print.css', - ]; - const parts = []; - for (const f of order) { - const p = join(DS_DIR, f); - if (existsSync(p)) parts.push('/* === ' + f + ' === */\n' + readFileSync(p, 'utf-8')); - } - return parts.join('\n'); -} - -function readHighlightInline() { - // Inline the assembled highlight.min.js so the output HTML can re-highlight - // pre/code blocks on view (purely defensive — they're already pre-highlighted - // server-side at render time, but inlining keeps the static HTML resilient). - // - // Zero-network constraint (SC1): the highlight.js source contains URL - // strings inside language-comment metadata (e.g. references to MDN). These - // are inert string-literals (not network refs) but a literal grep for - // "http://" would still match. Strip URL strings to preserve SC1's - // grep-based check while keeping the runtime functional. - const raw = readFileSync(join(PLAYGROUND_LIB, 'highlight.min.js'), 'utf-8'); - return raw.replace(/https?:\/\/[^\s"'\\)]+/g, 'about:blank'); -} - -// --- renderer --------------------------------------------------------------- +// --------------------------------------------------------------------------- function escapeHtml(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); + .replace(/"/g, '"'); +} + +// Inline spans, applied to already-HTML-escaped text. Order matters: code +// spans first (so their contents aren't re-processed), then links, bold, em. +function renderInline(escaped) { + let out = escaped.replace(/`([^`]+)`/g, (_, c) => `${c}`); + out = out.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, text, href) => { + const safe = /^(https?:|mailto:|#|\.|\/)/i.test(href) ? href : '#'; + return `${text}`; + }); + out = out.replace(/\*\*([^*]+)\*\*/g, (_, c) => `${c}`); + out = out.replace(/(^|[^*])\*([^*\s][^*]*?)\*(?!\*)/g, (_, pre, c) => `${pre}${c}`); + return out; +} + +function renderTable(rows) { + const cells = (line) => line.replace(/^\s*\|/, '').replace(/\|\s*$/, '').split('|').map((c) => c.trim()); + const header = cells(rows[0]); + const body = rows.slice(2).map(cells); + let html = '\n'; + for (const h of header) html += ``; + html += '\n\n'; + for (const r of body) { + html += ''; + for (let i = 0; i < header.length; i++) html += ``; + html += '\n'; + } + return html + '\n
${renderInline(escapeHtml(h))}
${renderInline(escapeHtml(r[i] || ''))}
\n'; +} + +// Build nested
    /
      from a run of list lines (2-space indent = 1 level). +function renderList(items) { + let html = ''; + const stack = []; // { indent, ordered } + for (const { indent, ordered, text } of items) { + while ( + stack.length && + (indent < stack[stack.length - 1].indent || + (indent === stack[stack.length - 1].indent && ordered !== stack[stack.length - 1].ordered)) + ) { + const top = stack.pop(); + html += top.ordered ? '
    ' : '
'; + } + if (!stack.length || indent > stack[stack.length - 1].indent) { + html += ordered ? '
    ' : '
      '; + stack.push({ indent, ordered }); + } else { + html += ''; + } + html += `
    • ${renderInline(escapeHtml(text))}`; + } + while (stack.length) { + const top = stack.pop(); + html += top.ordered ? '
' : ''; + } + return html + '\n'; +} + +function renderMarkdown(md) { + const lines = md.replace(/\r\n/g, '\n').split('\n'); + let html = ''; + let i = 0; + let para = []; + + const flushPara = () => { + if (para.length) { + html += `

${renderInline(escapeHtml(para.join(' ')))}

\n`; + para = []; + } + }; + + while (i < lines.length) { + const line = lines[i]; + + // Fenced code block + const fence = line.match(/^(\s*)(`{3,}|~{3,})(.*)$/); + if (fence) { + flushPara(); + const marker = fence[2]; + const lang = (fence[3] || '').trim().split(/\s+/)[0]; + const buf = []; + i++; + while (i < lines.length && !lines[i].match(new RegExp('^\\s*' + marker[0] + '{3,}\\s*$'))) { + buf.push(lines[i]); + i++; + } + i++; // consume closing fence + const cls = lang ? ` class="language-${escapeHtml(lang)}"` : ''; + html += `
${escapeHtml(buf.join('\n'))}\n
\n`; + continue; + } + + // ATX heading + const h = line.match(/^(#{1,6})\s+(.*?)\s*#*\s*$/); + if (h) { + flushPara(); + const lvl = h[1].length; + html += `${renderInline(escapeHtml(h[2]))}\n`; + i++; + continue; + } + + // Horizontal rule + if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) { + flushPara(); + html += '
\n'; + i++; + continue; + } + + // Table (header row + separator row) + if (/^\s*\|.*\|\s*$/.test(line) && i + 1 < lines.length && /^\s*\|?[\s:|-]+\|?\s*$/.test(lines[i + 1]) && lines[i + 1].includes('-')) { + flushPara(); + const rows = []; + while (i < lines.length && /^\s*\|.*\|\s*$/.test(lines[i])) { rows.push(lines[i]); i++; } + // include the separator that was matched as part of rows already + html += renderTable(rows); + continue; + } + + // Blockquote + if (/^\s*>\s?/.test(line)) { + flushPara(); + const buf = []; + while (i < lines.length && /^\s*>\s?/.test(lines[i])) { buf.push(lines[i].replace(/^\s*>\s?/, '')); i++; } + html += `
${renderInline(escapeHtml(buf.join(' ')))}
\n`; + continue; + } + + // Lists (consume a contiguous block, allowing blank lines between items) + const listMatch = line.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/); + if (listMatch) { + flushPara(); + const items = []; + while (i < lines.length) { + const m = lines[i].match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/); + if (m) { + items.push({ indent: m[1].length, ordered: /\d/.test(m[2]), text: m[3] }); + i++; + } else if (lines[i].trim() === '' && i + 1 < lines.length && lines[i + 1].match(/^(\s*)([-*+]|\d+[.)])\s+/)) { + i++; // blank line inside the list + } else { + break; + } + } + html += renderList(items); + continue; + } + + // Blank line — paragraph break + if (line.trim() === '') { + flushPara(); + i++; + continue; + } + + // Default — accumulate into paragraph + para.push(line.trim()); + i++; + } + flushPara(); + return html; +} + +// --------------------------------------------------------------------------- + +const STYLE = ` +:root { color-scheme: light; } +* { box-sizing: border-box; } +body { + margin: 0; padding: 2.5rem 1.25rem 4rem; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 16px; line-height: 1.6; color: #1a1a1a; background: #f7f7f8; +} +main { max-width: 56rem; margin: 0 auto; background: #fff; border: 1px solid #e2e2e6; + border-radius: 12px; padding: 2.5rem 3rem; box-shadow: 0 1px 3px rgba(0,0,0,.06); } +h1, h2, h3, h4, h5, h6 { line-height: 1.25; margin: 1.8em 0 .6em; font-weight: 650; } +h1 { font-size: 2rem; margin-top: 0; } +h2 { font-size: 1.5rem; border-bottom: 1px solid #ececef; padding-bottom: .3em; } +h3 { font-size: 1.2rem; } +h4 { font-size: 1.05rem; } +p { margin: .8em 0; } +a { color: #0855a8; text-decoration: underline; text-underline-offset: 2px; } +a:hover { color: #06408a; } +code { font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; + font-size: .9em; background: #f0f0f3; padding: .12em .35em; border-radius: 4px; } +pre { background: #1e1e24; color: #e6e6eb; padding: 1rem 1.25rem; border-radius: 8px; + overflow-x: auto; font-size: .85rem; line-height: 1.5; } +pre code { background: none; padding: 0; color: inherit; font-size: inherit; } +blockquote { margin: 1em 0; padding: .4em 1.2em; border-left: 4px solid #0855a8; + background: #f0f5fb; color: #34495e; border-radius: 0 6px 6px 0; } +ul, ol { padding-left: 1.6em; margin: .8em 0; } +li { margin: .25em 0; } +table { border-collapse: collapse; width: 100%; margin: 1.2em 0; font-size: .92rem; } +th, td { border: 1px solid #e2e2e6; padding: .5em .75em; text-align: left; vertical-align: top; } +th { background: #f0f0f3; font-weight: 600; } +tr:nth-child(even) td { background: #fafafb; } +hr { border: none; border-top: 1px solid #e2e2e6; margin: 2em 0; } +details.frontmatter { margin: 0 0 2rem; border: 1px solid #e2e2e6; border-radius: 8px; + background: #fafafb; padding: .6em 1em; } +details.frontmatter > summary { cursor: pointer; font-weight: 600; font-size: .9rem; color: #555; } +details.frontmatter pre { margin: .8em 0 .2em; background: #f4f4f6; color: #333; } +.artifact-meta { color: #888; font-size: .82rem; margin: 0 0 1.5rem; } +@media (prefers-color-scheme: dark) { + :root { color-scheme: dark; } + body { color: #e6e6eb; background: #18181b; } + main { background: #1f1f23; border-color: #2e2e34; box-shadow: none; } + h2 { border-bottom-color: #2e2e34; } + a { color: #6db0ee; } a:hover { color: #93c5fd; } + code { background: #2a2a30; } + blockquote { background: #1a242f; color: #b6c5d4; border-left-color: #6db0ee; } + th, td { border-color: #2e2e34; } th { background: #26262c; } + tr:nth-child(even) td { background: #222226; } + hr { border-top-color: #2e2e34; } + details.frontmatter { background: #222226; border-color: #2e2e34; } + details.frontmatter > summary { color: #aaa; } + details.frontmatter pre { background: #1a1a1d; color: #ccc; } + .artifact-meta { color: #777; } +} +@media print { body { background: #fff; padding: 0; } main { border: none; box-shadow: none; max-width: none; } } +`.trim(); + +function buildHtml(mdPath, mdText) { + const { hasFrontmatter, frontmatter, body } = splitFrontmatter(mdText); + const fm = hasFrontmatter ? frontmatter : ''; + const fmLine = (key) => { + const m = fm.match(new RegExp('^' + key + ':\\s*(.+)$', 'm')); + return m ? m[1].trim().replace(/^["']|["']$/g, '') : null; + }; + const title = fmLine('task') || fmLine('slug') || (body.match(/^#\s+(.+)$/m) || [])[1] || basename(mdPath); + const kind = fmLine('type') || basename(mdPath).replace(/\.md$/, ''); + + const fmBlock = hasFrontmatter + ? `
Frontmatter
${escapeHtml(fm)}\n
\n` + : ''; + + const bodyHtml = renderMarkdown(body); + + return '\n' + + '\n\n\n' + + '\n' + + `${escapeHtml(String(title))}\n` + + `\n\n\n
\n` + + `

voyage artifact — ${escapeHtml(String(kind))}

\n` + + fmBlock + + bodyHtml + + '
\n\n\n'; } function render(inputPath, outputPath) { @@ -104,93 +287,35 @@ function render(inputPath, outputPath) { process.exit(2); } const text = readFileSync(inputPath, 'utf-8'); - - // Load vendored libs (deterministic — no network, no timestamps in output) - const markdownit = loadVendoredScript('markdown-it.min.js', 'markdownit'); - const markdownitFrontMatter = loadVendoredScript('markdown-it-front-matter.min.js', 'markdownitFrontMatter'); - const hljs = loadVendoredScript('highlight.min.js', 'hljs'); - - let capturedFrontmatter = ''; - const md = markdownit({ - html: true, - linkify: false, - typographer: false, - highlight: function (code, lang) { - if (hljs && lang && hljs.getLanguage(lang)) { - try { - return hljs.highlight(code, { language: lang, ignoreIllegals: true }).value; - } catch (e) { - /* fall through */ - } - } - return ''; - }, - }); - try { - md.use(markdownitFrontMatter, function (fm) { - capturedFrontmatter = fm || ''; - }); - } catch (e) { - process.stderr.write(`render-artifact: front-matter plugin error: ${e.message}\n`); - } - - const bodyHtml = md.render(text); - const fmHtml = capturedFrontmatter - ? '
Frontmatter
' +
-      escapeHtml(capturedFrontmatter) + '
' - : ''; - - // Determine title from frontmatter slug or first H1 fallback - let title = basename(inputPath); - const slugMatch = capturedFrontmatter.match(/^slug:\s*(.+)$/m); - if (slugMatch) title = slugMatch[1].replace(/^["']|["']$/g, ''); - const taskMatch = capturedFrontmatter.match(/^task:\s*(.+)$/m); - if (taskMatch) title = taskMatch[1].replace(/^["']|["']$/g, ''); - - const css = readDsCss(); - const hljsInline = readHighlightInline(); - - // Self-contained HTML — zero network references. Determinism: - // no Date.now(), no Math.random(), no timestamps. - const html = - '\n' + - '\n' + - '\n' + - ' \n' + - ' \n' + - ' ' + escapeHtml(title) + '\n' + - ' \n' + - ' \n' + - '\n' + - '\n' + - '
\n' + - '

' + escapeHtml(title) + '

\n' + - fmHtml + '\n' + - bodyHtml + '\n' + - '
\n' + - '\n' + - '\n'; - + const html = buildHtml(inputPath, text); const out = outputPath || inputPath.replace(/\.md$/, '.html'); writeFileSync(out, html); - process.stdout.write('render-artifact: wrote ' + out + ' (' + Buffer.byteLength(html, 'utf-8') + ' bytes)\n'); return out; } -// --- CLI entry point -------------------------------------------------------- +function parseArgs(argv) { + const args = { input: null, out: null, help: false }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--out') args.out = argv[++i]; + else if (a === '--help' || a === '-h') args.help = true; + else if (!args.input) args.input = a; + } + return args; +} if (import.meta.url === `file://${process.argv[1]}`) { const args = parseArgs(process.argv.slice(2)); if (args.help || !args.input) { process.stdout.write( - 'Usage: render-artifact [--out ]\n' + - '\n' + - 'Reads input.md and emits a self-contained HTML file with inlined\n' + - 'CSS + highlight.js. Default output: .html next to input.\n', + 'Usage: render-artifact [--out ]\n\n' + + 'Renders a voyage artifact to a self-contained HTML file (zero network).\n' + + 'Default output: .html next to the input.\n', ); process.exit(args.help ? 0 : 2); } - render(args.input, args.out); + const out = render(args.input, args.out); + process.stdout.write(out + '\n'); } -export { render, parseArgs }; +export { render, buildHtml, renderMarkdown, parseArgs }; diff --git a/plugins/voyage/scripts/vendor-playground-libs.mjs b/plugins/voyage/scripts/vendor-playground-libs.mjs deleted file mode 100644 index 1f90517..0000000 --- a/plugins/voyage/scripts/vendor-playground-libs.mjs +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/env node -// scripts/vendor-playground-libs.mjs -// Reproducible vendor script for v4.2 playground render-pipeline. -// -// Usage: node scripts/vendor-playground-libs.mjs -// -// Pins (locked per plan-critic B3 — never use highlightjs.org website builder -// or any other interactive UI; this script is fully headless): -// - markdown-it@14.1.0 (UMD bundle copied verbatim) -// - markdown-it-front-matter@0.2.4 (CommonJS module wrapped in IIFE) -// - highlight.js@11.11.1 (5-lang bundle assembled from CommonJS sources) -// - dompurify@3.2.6 (UMD bundle copied verbatim) — v4.3 Step 24 -// -// Output: playground/lib/{markdown-it.min.js, markdown-it-front-matter.min.js, -// highlight.min.js, dompurify.min.js} -// -// All three output files are zero-network browser-loadable scripts that -// expose globals (`window.markdownit`, `window.markdownitFrontMatter`, -// `window.hljs`). They also work under Node.js dynamic-import via the -// pattern in scripts/render-artifact.mjs (UMD + global-eval). - -import { execSync } from 'node:child_process'; -import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join, dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const HERE = dirname(fileURLToPath(import.meta.url)); -const ROOT = resolve(HERE, '..'); -const OUT = join(ROOT, 'playground', 'lib'); - -const PINS = { - 'markdown-it': '14.1.0', - 'markdown-it-front-matter': '0.2.4', - 'highlight.js': '11.11.1', - // v4.3 Step 24 — pinned ≥ 3.1.1 (PortSwigger HTML-comment mutation-XSS bypass - // was fixed in 3.1.x; 3.2.6 is the current stable line as of 2026-05-10). - 'dompurify': '3.2.6', -}; - -const HL_LANGS = ['yaml', 'json', 'javascript', 'bash', 'markdown', 'diff']; - -function vendor() { - mkdirSync(OUT, { recursive: true }); - - const tmp = mkdtempSync(join(tmpdir(), 'voyage-vendor-')); - const log = (msg) => process.stdout.write(`[vendor] ${msg}\n`); - - try { - // 1. markdown-it — copy UMD min bundle directly - log('packing markdown-it@' + PINS['markdown-it']); - execSync(`npm pack markdown-it@${PINS['markdown-it']} --silent`, { cwd: tmp }); - execSync(`tar xzf markdown-it-${PINS['markdown-it']}.tgz`, { cwd: tmp }); - copyFileSync( - join(tmp, 'package', 'dist', 'markdown-it.min.js'), - join(OUT, 'markdown-it.min.js'), - ); - log(`wrote ${join(OUT, 'markdown-it.min.js')}`); - - // 2. markdown-it-front-matter — wrap CommonJS in IIFE that exposes a global - log('packing markdown-it-front-matter@' + PINS['markdown-it-front-matter']); - execSync(`npm pack markdown-it-front-matter@${PINS['markdown-it-front-matter']} --silent`, { cwd: tmp }); - execSync(`tar xzf markdown-it-front-matter-${PINS['markdown-it-front-matter']}.tgz`, { cwd: tmp }); - const fmSrc = readFileSync(join(tmp, 'package', 'index.js'), 'utf-8'); - const fmBundle = wrapCommonJS('markdownitFrontMatter', fmSrc); - writeFileSync(join(OUT, 'markdown-it-front-matter.min.js'), fmBundle); - log(`wrote ${join(OUT, 'markdown-it-front-matter.min.js')}`); - - // 3. highlight.js — assemble core + 5 languages from CommonJS sources - log('packing highlight.js@' + PINS['highlight.js']); - execSync(`npm pack highlight.js@${PINS['highlight.js']} --silent`, { cwd: tmp }); - execSync(`tar xzf highlight.js-${PINS['highlight.js']}.tgz`, { cwd: tmp }); - - const coreSrc = readFileSync(join(tmp, 'package', 'lib', 'core.js'), 'utf-8'); - const langSrcs = HL_LANGS.map((lang) => ({ - lang, - src: readFileSync(join(tmp, 'package', 'lib', 'languages', `${lang}.js`), 'utf-8'), - })); - - const hlBundle = assembleHighlight(coreSrc, langSrcs); - writeFileSync(join(OUT, 'highlight.min.js'), hlBundle); - log(`wrote ${join(OUT, 'highlight.min.js')} (${HL_LANGS.length} langs)`); - - // 4. dompurify — copy UMD min bundle directly (v4.3 Step 24). - // Mirrors markdown-it-vendoring: npm pack → tar xzf → copy - // dist/purify.min.js → playground/lib/dompurify.min.js. The UMD bundle - // exposes `window.DOMPurify` for browser-loadable use. - log('packing dompurify@' + PINS['dompurify']); - execSync(`npm pack dompurify@${PINS['dompurify']} --silent`, { cwd: tmp }); - execSync(`tar xzf dompurify-${PINS['dompurify']}.tgz`, { cwd: tmp }); - copyFileSync( - join(tmp, 'package', 'dist', 'purify.min.js'), - join(OUT, 'dompurify.min.js'), - ); - log(`wrote ${join(OUT, 'dompurify.min.js')}`); - - // 5. MANIFEST — record the vendored versions for audit - const manifest = { - generated_at: new Date().toISOString(), - pins: PINS, - highlight_languages: HL_LANGS, - output_files: [ - 'markdown-it.min.js', - 'markdown-it-front-matter.min.js', - 'highlight.min.js', - 'dompurify.min.js', - ], - }; - writeFileSync( - join(OUT, 'VENDOR-MANIFEST.json'), - JSON.stringify(manifest, null, 2) + '\n', - ); - log(`wrote ${join(OUT, 'VENDOR-MANIFEST.json')}`); - } finally { - rmSync(tmp, { recursive: true, force: true }); - } - - log('done'); -} - -/** - * Wrap a CommonJS module body (uses `module.exports = ...`) in an IIFE - * that exposes the export as a global on `window` (browser) or - * `globalThis` (Node). - */ -function wrapCommonJS(globalName, src) { - return [ - `// vendored by scripts/vendor-playground-libs.mjs — DO NOT EDIT`, - `// global: ${globalName}`, - `(function (root, factory) {`, - ` var __mod = { exports: {} };`, - ` (function (module, exports) {`, - ` ${src.replace(/\n/g, '\n ')}`, - ` })(__mod, __mod.exports);`, - ` root[${JSON.stringify(globalName)}] = __mod.exports;`, - `})(typeof window !== 'undefined' ? window : globalThis);`, - ``, - ].join('\n'); -} - -/** - * Assemble a self-contained highlight.js IIFE with core + N languages. - * - * Output exposes `window.hljs` (and `globalThis.hljs` under Node). - */ -function assembleHighlight(coreSrc, langSrcs) { - const parts = [ - `// vendored by scripts/vendor-playground-libs.mjs — DO NOT EDIT`, - `// global: hljs (highlight.js@${PINS['highlight.js']} — core + ${langSrcs.map(l => l.lang).join('/')})`, - `(function (root) {`, - ` function loadCommonJS(src) {`, - ` var __mod = { exports: {} };`, - ` var fn = new Function('module', 'exports', src);`, - ` fn(__mod, __mod.exports);`, - ` return __mod.exports;`, - ` }`, - ` var coreSrc = ${JSON.stringify(coreSrc)};`, - ` var hljs = loadCommonJS(coreSrc);`, - ]; - for (const { lang, src } of langSrcs) { - parts.push(` var lang_${lang.replace(/\W/g, '_')} = loadCommonJS(${JSON.stringify(src)});`); - parts.push(` hljs.registerLanguage(${JSON.stringify(lang)}, lang_${lang.replace(/\W/g, '_')});`); - } - parts.push(` root.hljs = hljs;`); - parts.push(`})(typeof window !== 'undefined' ? window : globalThis);`); - parts.push(''); - return parts.join('\n'); -} - -if (import.meta.url === `file://${process.argv[1]}`) { - vendor(); -} - -export { vendor, wrapCommonJS, assembleHighlight }; diff --git a/plugins/voyage/settings.json b/plugins/voyage/settings.json index 4903705..2f9a447 100644 --- a/plugins/voyage/settings.json +++ b/plugins/voyage/settings.json @@ -27,12 +27,5 @@ "enabled": true, "statsFile": "trekresearch-stats.jsonl" } - }, - "trekrevise": { - "defaultMode": "default", - "tracking": { - "enabled": true, - "statsFile": "trekrevise-stats.jsonl" - } } } \ No newline at end of file diff --git a/plugins/voyage/templates/plan-template.md b/plugins/voyage/templates/plan-template.md index 40b0ff2..f249ff4 100644 --- a/plugins/voyage/templates/plan-template.md +++ b/plugins/voyage/templates/plan-template.md @@ -14,22 +14,6 @@ source_findings: --- --> - - # {Task Title} > **Plan quality: {grade}** ({score}/100) — {APPROVE | APPROVE_WITH_NOTES | REVISE | REPLAN} diff --git a/plugins/voyage/templates/trekbrief-template.md b/plugins/voyage/templates/trekbrief-template.md index bc7088c..b35d893 100644 --- a/plugins/voyage/templates/trekbrief-template.md +++ b/plugins/voyage/templates/trekbrief-template.md @@ -1,19 +1,3 @@ - - --- type: trekbrief brief_version: 2.0 diff --git a/plugins/voyage/templates/trekreview-template.md b/plugins/voyage/templates/trekreview-template.md index 3a5491f..a47c7cb 100644 --- a/plugins/voyage/templates/trekreview-template.md +++ b/plugins/voyage/templates/trekreview-template.md @@ -1,19 +1,3 @@ - - --- type: trekreview review_version: "1.0" diff --git a/plugins/voyage/tests/e2e/snapshots/voyage-playground-dark.png b/plugins/voyage/tests/e2e/snapshots/voyage-playground-dark.png deleted file mode 100644 index 3ad1ce7a00d4c7eb274b673778d05e457bc2097d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 94381 zcmeFYWmH_t)-H?&39i9IfZz}yxCM82cbCRpLV~-y1b26b5Zv9}-D#w0Ztt_tKIgsP zkNfNUbH})AtkJ!?*P6AeX3aTk)>BV~DauQrA`>D*K|!HPNs1~%LBT^VVFM9iAYYE| z?^U6oFrcJFg;YH<&$8gZ;m8pUrd-5hiS|Gti3!2sVlbTHVYkD!|LcH-?H}mDgTt+r zJgx%zH@A>F5F_O6jNZOH`T)%>Acwj8;Ts=Y{mk=#TI!Ebmg88(jUe(+W8vdj)W+|- zC_)%;|2p5?>+0%m-dS6ohz9zZfw^XOM3p`L1!Sa|3L~w8J3fnGbbnWMDp&(H(_K!m z{4E3d^vA1(t1%U?Yukhl{)qVF-zQj$lHi|f#2+;9q5IIE|8q0+_a_&aFF(?{;XJ=1 z{qt0)GBlFmKR*!jRKLNb{--z^+$rM!-rI?}Ui=Kjw)a0ult!ZbKa}MB&%L}U$`qEE z@+DS1A$l3{IC5zMEUT~?DNMS{$zgy0K8)_PiV6l=S}Q9n0xJRnT983-vC-_z3>)jr zjLLORL*$(c5)u-NN`kV|qVYei!H8iH|5RmMo=TU+wm(lmOAt&GBspw4Qc88aVlE{) zB90JV-iZi}gOh}fy@MQGRFIcf#6(|BL&ccZZ(3MfTu4b(R77dPs-=t-&+4?4Ur|zm z0-c$iJQ521AMNB}6qIR1bGn%!3Jb$93Ni60GL7VrhZ7-5(XvOemeSHXPAkch5{Zaw zTfYM*{CaD@5Vy#T9=n5!>^FuKrwItG#MHYPxOAnZ zwS|;uX<5iGH;jp3NNi>Vq$aC0&&VX}x&0C$KZ78W%2MQz8n-GSGS=Ra!@?yNWA?#Hl63CR>29 zw2p^mtx*y_q~n4k!+dDt;>P~16e?0R*UHcuW?7cpGX2lwxl6%N)B}TMr8eUJ=##km zwlG{W+5xoc;rv2BK5EvPs#2m5NgeS@N+yRY;NLWGf^l4_8Y=2oXH%=)>wi>B7T|*P zxc+vU9p=1}0^(QxNLn3S_<-v=RGBOsXMPX#!+G?YP z?++KstW*rg#v)y;(ufnlU{C0?gN5tqU5Vz)wcuI4#VUe&bHl}Ke(zNOD6`RI^25i| z<#y^@gt&x+c}5*|2^}4f$cSZfFFA5}_?d!<$*$+^0h4w|NkulPN(Bn>*HA|6hsoJl z=ck+fO6|s-PsDIXgXTUuMa(H}tl zrFa_dt@UHuvfNM1{TQMwy4DT(LDk5bILt}B^Y%+#Skd)u+tS11Le6x$CJ78~bZ!$;F|Mgid zKQg#J;&x1n?d9&c4`au0i-G*7qg!ac(&hm9Y^t?gr1N=cirHeS z{_>misP(^w1dv|^s+zC{53?)VIIcj4kY`|X{lf6&Hx)fG(GO?FJ&7N5$G+OjHe_v- zDtiMV-7n zT#6u`e!RZA>T$cp8;6gv-pu8Pd3iV=1*xU81C-jEM5?~g(h{)1e^K2SXr!kqEC}|9 zoERHp_~Iunp5Ab`iP6)%A--*ZfbIHVktJ=NldmiwO3BhKI|roUf~!^P782=oXn5!9 zx^Y~k|1=0!laP?mk-1i>)0~%|KRz~wf{L2v9|8$R{SkPTnq5Dh`1$-mEp{1Al{$^~ z%XRbHeIb~vwvS&tatVYfwOHvQ&DC<-qve{~zqUskhDN9BUQK-N)4{t^e{r@{ACIRU zK^TCznW?E0voj+#Gc(1g_ik{oPrJjzUOSZkzaosEU{|lB<1m3Q@x5806NjMc5C|@>LiI*8`T#{d^@U zEs5LlwtIcBRsJ)D&q_pI|{Uj^vOxm$AYCtxt zV});J`p@9#$Vd>AJSg(+uAAa}-|p;&kBkF|_WQh+lrDpA>rR;eHgs}A!Y<5dd&}15 zVzqvs&QS1Iy9L|f87c=vz-q1CRuA;Z2%j?!;X>{1prKH}d?4?_%nTMeqD6?>Vog=8 z$MBAg{^G;g^3U^OmCq)C!Dy1!$0?Hx4nUjV?(+$IxvQ&7lig-8A|BqdJG&FS3<}}? z{HcaUrG7uR%x%7-$p+j?hyXg7fX|<|GztOvz(4fr6-O5repgeuu!nfwWzc#4yS zq!02L9OFYvmeV_8Lu@;Hgt)q$UiZhd#W}q04@t*tLH~F5g=*s^Y zw(|=N4zyjXVWDEjT~(j2xDo$;L%?S`l-n1=q}TP;Xh#$~VZ=6z)#p7yQ%nvQ(~=>N z`|*!&y0kAUqFHsz*_?o?tgQ7eUvFmJ4w^Q@-mC3CN6Bu#*Ou7p-9d7=BpP$>o?kx~ zBJ|qbFN5>5X(_R^)t{p7`Cqqm)F|eW+ro7vopto2kPa#)0oU93&>N)Mk&$6; z9!^!2Nx!ULEOX_XCBX0GuOX2RR#TedpH~^qAsTW_z&l7*dqE>!sK5M}kBob*X*u82 zH-)*o<_%n2pdI~dfC%(BYk!kd(JC^#&v71SxKFbFP?q_C8l$lN)U~2*lS5|p>MQqJ zzH7}_N<9?diVX`-UDDMO3%rEZC+W6nyhnJ&C; zc^w?ZKSn0s46xKc=+ETIHmNICjJ7d$aStaUZhApbEjI(rUg#>xY_Cb4Ud=N}I>@g| zJI!=l1m+fj?iP{=wN1T0Vtnl?oqjEKGb+!FL8~pJv5llQ1zE)qf^SK--n}$bWH*J| zQoNV5ZD^P=c5d&er{x=HsXDAb<;t+Oq#GtC(&qQld!=TxS){4{{rLwNiTD6;FfX^Z zbamg+)=6@=?0`)oMme7!y|EMhnVdc=Zlz=(cn1f*L|@-ip}VZ*N@DgLK86m7i8|0c#^Pt&Uk#CCtIc4$l_q}Y0~CBV3bwvZZ(z`Sd`EIeM@JbvW+i3ybYA}| zoQ`r<)?2)>l#~>M8bc!|WtPW}?`4FAf0*#6sN1cwbYSKSqe`oy=Af>g)io^R#%4+{ZfZlyWGL<(O>MGnVt2d2ss7eXfWM-<`cJG zPm=JseK)q6`UOqbC4Nr`i-7uLPRQol`^OJ=N(u_yMtwPGd*V?<P~=u0-HrVZBf zbZ2U6N|mO{p5rI;=4xX#8d73mQD-X!!;xBSwsv;=`#2|arS-=5bRV=agv2>kXTF#$ z$i{4wa)4-8qNAg4@fj?a>&*NXf0TdwwB&JjH2wMlteNI2He6U(5Z|d>Z6Ia$!V%4C z3-&5ct}K$vN%n2&`1G*j-vUj@ZVeX|&NZ6M+}CXj$L4%{f4bOcwOSNel$;x9lv1U) z2w3Zpe^yhB)llBiODrfTC<{-I$5OxyYx9wMH(gNZk#9|o^u z&6Fmr92HR-gUXN|jPaQ)LlN zr(5iNgexkVLdE=m{^d0tz8tPr?-fOYHG6q!go7Sf6;aD7Oly6Nl)=4AU^7=C|HlTW zwp!YbxU#Uanwr|lV9nL%aUG2k)u&?$vNuj<#xiwxyKLWt-zF6rRhemG^9b$$2?<}o z-pW{`*65!@id)K97K z>?`#ZhR2dl@VM7cSBV?sDKm9PrfzN&v?i%@qqcTTV=VP_jD<6)Rds>E_W+=|{vr_}7Y#{8H<`c2u76jTAANE_p|w%~_aiQMoyW%_p20M}nVc{Js8 z-bRUX2s$LXTwb+Y*2>N%MSq}xm$w#^5=X7aCkrN|)k!`~Ga3{WfscZun?&#NnOq#*j%QkBJUm)D zJAJ#sub3XkSd%{yu-~VAOne6x!|v^N@c7;Re6ug3p`nrDH6amA+*n~f+#h(A(TkrW z5kp#jxwBRy)OS=u!^p_U1QY0vt+Dv)*GG3CX%rDh_E$$NnBvJ)G%?CM?aWMK^69uv z;N|9Kw|maGPY^P$lbFp+!7wC`NoTimJK3Y%2ohm0eUrt=9_FkzXtvxiesqYrjN2r) zTy5G-)2XR7?nmBxx=Wj_iX!3!EP#4{A~*mhG6c!I$!iB938SKFt{f5v=1ONp4FvvB z=XH93VOC{Q7<%0d4KIiQH;J?ZxRI|L9aigYsMZ$1s&$Ovh!ln&vGiY>;gu`3*-1#U zfBU?is8#8G;jCeLPGd9Y;pTEXo5IXXT+N7!bKIw^Hfp&FLHgpu+1MHce}SqnISRi( z`=0a@7z}=We)!ob=h7P<7V7k!lk^>56JF-&JTD{}4KgE_K&3t8bv*LcUCCP9)LFIg zW^J4|J(p#lcn&lq;&BsFh_JXcanpZVDhYl_7vfWRfqlDVv`$LeC;Gl`Myazv=Y5C@ z01bzO=e3uUf&{slXnoz>*-CV!z_VuaQ033++N*qm1C#~hmsS(#ha|j%>v7PiH%=v&hKdGs-iIe9T>5*Jsf{R9rQ7(7$ja@^=A7QozmrujNIB zm1CzzyRJ)>Zp&9Ks*@TW*ONNlTF)Vr>X{xkOMczq*nmE26rogl`1-o=*US+Tf8v9d zBl0|UgZn?i2DII?Mj9`!3LzKXyB&|yMme$BuJ=<&7Vf3AlFVFhxeIFu&J$gn3>1RJ zc4n4~ZSQ^Fo%x+NOi8Her_c9sh|`(Po2KZZq(;Bz!h2GkQa(D5-E92Fe8)Y;A*vH4~53amhUAo>tkTm z`Wix~dk10DWmH-=)?8!X?+_ABKHkTVaPg_Qd$H9+O|y9+D)Mkmn)h(zG_n^A7;O)? zq$r!07W^-y#&szl+h`6Nt1HUsYy|F^m^ub3UJkQ^6aZ>UWu_G|NX#1ND~*?H?LW^~ z-R<))5n>QkB=MNFiSfx6qa+rON{;sFliB=Z=H?Pd@h5?2tJR%4hZA~jO%u!8hvRd& zfJVZY>D_!2<0gAe3VNLyL-WV_h}jt@lqzrn>6Rs}RM-mc{E4c1%vMaOd3X$cil^5= zZ8in(ezl)3zPKT%UT!>nN)M80^Z1={iK4lz3`7xO+8eB*y?J?WN{1@YXjBoOI+QCH zA~%`r8egupqN2x8`OlpOyuKJ&3N71h){0C8Dz++9xuxDMRO%=zEAQ^?KoWoYR2J{9 z=G&=W5sUyWEiH{|eczK8oNNo{_uDYRuivNEz1^{IfG-ObR@(4hBttRV7U z2j~59*?<&-k8W;_SC==q^j*HM$lmk8s6>hsbR%>JFwczLYsg|UI_*-kMdRt583N8c zd{u}@XlN5Q*JavChhI>hFu>Me&6-#E4wIa5qi)RhV)#(xCdcUVDfOfz zwogun!dH+LTL-jnu5gz>?CZnyt;FKjO`l@F%yM#F*tkgh9fnIwb_1D$*Efc8s&!rm zHFqGWo}POhHPXUY<)m1{1^rRK_NO39eNPfAjR1=c-&b~W7WVvVl~xY+h9uL%hqc$> zwF91rN|wv34De>09f+#H)aN-{SH%#+)te}bDofV!40KDD3Th?6z|Hz|YP#`$Z~ti} zMgW%Aa_$#OwTSDE)Q5tWibTpxYdZ)=^B7^}NV|p{`lD??+}Qfyg4bY_W;pR}n`p9{ z#vu(|@DJUHbX{7AAn}HYv#(hZ+}277%UuKz6%bsk-Z5Qhwb9{egdL!%S??7U7Z(>0 z&|9}Zx?G#!pH15NfRtPk8~s{2e=I z539`pPukJpTQ-wu|J-+uxMv%XcSsD1mY0`iq^Wy8fV8SW-@b=+`Ai=jpO%-Gi}-i> zy#}KO*xugW&Mq!Cf8q3Zcr!e((b9+=e>x& zhu$sg0t|2!b$%92z&BU z14FgRyP)A9KD6-tgPnsNB}EkAfN@H8kVVpKT4H+d1kbzq$F3=NBf-Ou99A|@{9Yb2#``37cQ$8^`Pk(6??vN7V_3S|aLUQU&YiaQL3r>C-~%sd$&+yq zaY9LXl8WCGRrb^BSE~&i` z56*-#f1LepL{(Oi-*P-aU8EV%_t5+EMP6RM(Pj||5v9U-AR%k*d9rMzW5(2%i$MxB zyy^5!pEPfOr%(o?XJTSPr`g_S>iZ!|heI4ESSx^;?^iVzwm&IR&ed&l>^{1_gqRaK z!Lijb0YRpwF|}u2S!+FlKiYr+lLVg(KO~mh%4YE-r9HE)wftZ)t@)M1Mz5lZ&qq%N z{Ftc&vQfZ8ksVNIJkx8Xxw;X{A5p-z<7d1to~8=RN!&WFL*5-jK|=NIAtN9q#e|l7 zZ#7~yk;#47NxHMM+k(Mm?%dz%a_yAU<*Rs1Cg|5tW}lQA3&n;^A)a+G?q>8}o#rX& z=ahD61oIwL_?)J=K3jJdOV}}|foO~B?25y-FQZCSkc8)rj*vQXbzniF?S5K%c2aV7 zXmg~25PuYzKRFc*a1-$Wus`*+qoYB$Pn*L*fuW(aHJ!*`bHsjWQJpioC zGVRzb;O6bvrokhR-P3-|D!_Lb*Ffa@kV}Cp;R=8%lGL2L{F+nt?&;yHES!4F+`hlF zWJ&uYn?GJvblKSp*`~#`dR&y0bj+c*%66Rqn5#ibZJc9)hyV0MCt0P6e^cV>iW&5^ z-PQ{Xx}EG&*B(%S?ihc04{D$wBGC6B_1kaM9DO(#Y>itu31_81d(`uNebw_YKBB`H z?>NP6d*68+yEKQsE%o!E15Z{WP~|m7sY5OM9MX_xTwC9 z!*3)egck|;8sKV4>p9(S5#0b%53xp-ZU9tMefj7ScZ zRTE=R1`L##n^7^>G<^XTHZ?U#ppgkUKSMI1saQcUHxC^T17GHQ27n@({n9{Hl_$b8 z9-fE6b@i65W(%IHt84A`z$I!z2n@5q*UOF0kigi4q<((Tu}ahM&#ca^$w?_p5Bv{Y z?iXt?`S4xuMWdX_#9`TOt@py6k7uY5w^kZ$5Z=K_OG|rsuJ_CaVCKu;DjW6|T@OZ` z9=~Q*7?dhgiSxB+mzb+Xxn_uj>!{K15IqD4*ZrvG4Z1|G9sTZ)5%X?a>cxcqSa9s zyjaEm7j_>`)Z(-Ka&D}#AZ%bYkp{HgdbuiCD!AiGnj0YT91ZT7B*05S?k?Y|Rd`~a zg5V2hil+3i`^ehJVWq$Ajs~=m_u2O}@o9K99{LYkI;OLI_TV|1XN(J5>M|3ZaZxi7 zsTVX-Y_!#G_Y!JUy0#w^;$x4LG!#TwLVT`XhfW!^k~FwM^lvOC|6lUb1XS}maLMX$TkWIU43qQ+3Ir=mLa*GR!Xh<;9hd_4e9CqRU2ZvirR6W*XBBX#k%$jHph9fey|FM})m5n7r8O8mo5 z^G-L&!&29+0>L*Dg0%0@zeF-!4IAp-+jow`@{SjCIQed7(4B>)rRmmwE?KTndha`0+Frm4e%!Ky;>(Ox z;P-my*>-ZOTW@p29>bmE0^3dJ!Ji6>W*)N@L)OQlUnubrC=-6d0-y#UqfDFY`N%5? zuV3cyElWY(_TeEqZDDzSpD#{3WacrXJk$(0bJ!I1yEM>J3d!baA5?54CnZ^J&3x3zbcgtdFA)^^$}e(%&~1zn zIsAf^J~G2nEXKxB5l^E{{ix#V^0HbL!-Di8N;lq*pPsW$=^Ud~L0F^dwGAagoi7`U4SC0)fOR7T}sqcv8=Fm~Al5Lc+ zd~9{Rw7%BPtu6I+bZHSh&dmCwq>WY6s2iXLqlK8XipY_e9{jeJ| zLPI^kAa4Ad^HYr$AR&i6&B2HQ6)HRXeQzJ%L?%ywK)oF!FIz-em{jW^+h?jjQlmps z(qZh_n#2%z+RW560n0h(Cw=d zmme^uqO0|tN5#Y#eDR0o*plh+ydAhF;%sPa%s0tvMQnFJM|;Lltv4TcKAeMjh`A{GO2iv!jD=<&WHD#*gjADt(hjY@zW-Y&n1UmUevR1K&fpfk*q4G?^M$ACq>gMKmcjxn+c9)N*zFIeA z_3ck!Acx(`*=jRBrh1iDR1aeYr~lK9x33K+V8iG6tTZxmY(kGWg;B>1vVzB&F1!Hl zbb0z@N~h}U;qfSvb9T-5w?}F2Aqe6oCCX> zyBjl!5gtv6^T)w?QUBI8!T;j79fXcLHmXaa;p~axI?4AqxZsioC`|2Y~osV{Tc5j zxGSzMAA}CfE()!~FYR;JTp!8{MgOWG9OPT$%)-M+M8=}q!>!_23}NYcCmgvJ8Y5m_$lc7 z43~8bJQxo_7sM~UeR$CCuyk|>Uf?C;9Z^+lwT=ulnQH~qR#;4A20R3>E)NYMX7}6^ zEnIH;e_tiS#%8m9yb8mnhU9tfXG<(RbStyBBfr$;Q|`f=jq0*H>p8%u~bnM|7`GA44cK+2VVQkqR*2b zc`P0>>C|M;&(8_2ue#Fo-qVQB{T{)kudtc>3%8@-zzQ?c+BA?!#B8)*=y6qW4l*@0 zk6H#hfhIJOm}gI{$T>#YI7N%>34!eos^1D?6m7Y0k#6xSC>)U3VZL!RI2my zy2!X&5Zr{VXsvqhVXD0KvG{x@qsP}cB6;akaCT%g{vsRp&iF6dfc8;yygp(0ds)}z z)|Re$F;{m}^#uIshs0N1hl{>QmCHJGE3Ipp#H-^hNyD|cW;S{F5h)quHalKBuWWjA zAL-!Lfy0@2tn?99uEwpZ$_<07;`~oUIq}(#yS|K*2D?9BklyIfLN-+;)E;!kCi^%& zd^dUwoVYL5Hofspc!2@Ic`hj3KXchmieCbFFI~r4`blVCk11sh_}s3Xy2Z^ zSpzv$8w}Nr6aXC{mN|yH^mu2rI8ZoDPkyu@aZ+qN`P{dw!)xGeeWg_fC$X;#$Wd@I zQy{@6eE)DVCx~<@*B9XcfK7<_KP(6&6w$@No$#8cOO1$#P(mt7LAK*h#>+d#KN3c{T*iU2k^<0)dmBhP|$IIMeXu z6&0E8+)UTgGc(WNS8u<@kLe6zBw^{;l$8FiODM#xq?L4S+{vqM+?M;t?s*h4l3xa? zSBFO=FK(Yz)kV5AVEerNHbd-jY_x2XE5^Pa^y+?YOw~9~Nzte&S6N)kTS-POueVgQ z_$woc1x5Q+bI}Y*Zsb^#;id3g(_E^w**CfzHdl8XSYHkOY(-EGs&on-vaNrhRaw(K zq3?}ZS(9FM$ONWf)#&5&<+Q(8>#wtffHd@xCfb2O+@O!$B26G{)e_q$@~fmHuGVMm zx;iJVGy*P41~_%L`$XWGpy8& z;$O;$>D_vH?SRs`{Y>2cXO8T1kiv$8S zbLj<#Kbt)obQ*^vXZDP(=0|FA9xP5RCN8R_==hwSgNbt9M@A0ny)|}SFzsi|&-Fhf zj|%Dd`DjS#ZjjQt-M=3zS7!vMZV%kk7x+PFxuO1{1r1|LA)p0UPXDj@92H{(X&sYK z@;rDwAR(TW6ZILTKp1B7la$9p$$eTKC6bz@lFKr}bO`xR>=w2UNKZ?fl=EyRor}Q5 zY<_S6A$OTQ${0lv@lLF+wkwA4a&t54bjeB^wz+9)KJJAj`~f}FUu!tHyOTkf0361~ zCT*_A$u`mtZ>{oC>CzuKO~9CSKbTm_m|41^rKE=QxM%*w9e(mAHAF)dJBC2dki77K z(^^AKpN^l6o~7~8z^zUhtCWCh)Q5q#w`53$y3ibPeCd)NS!f8B0mcaYPG8cG)DX{8 zj0#??@XzBCN^N%<@_0o_MS3DeN+zW>?WOV|^n{GE@y>xSp3Uba##cHO4Lj=9nRo?OFjlDU~#d##&R$wyq^TwH{dobN{ROHmv9~mjINeGx+HUQBj5HfVAa!96^x*4ZbrLdpu zqe2|PweqJcngn5W%D)^W@Me*}M^q|*ZY23;pvXY5b8<)$AG6pJqC9viGf#M`g7~|w9M4hM4AQDKI2;`nF4AvDg0suBqUZiArP8!9+ke` zxN13NuKn+l4=l*(krnBI4ScsLL(&Qp(qVCu(WW~B=3)qve>zP5=5~f+DSW!z?X1g# zzhaJ0#E21;lG@Pl^(1>pN=i}Rt2{~1&JGql8fdGC;x~KZ)lL+K;%AvvUnT%{bp?ZDXkm2^1rm_Z>;Pbe=(mC1^QXv zJ>-1m=V$x`BKj$46sidq@kE5PieU-qkiSkE)S*AV=7>88bEIHKNhs{UhB+Y&QF2AJ zt_jm=>U|^ut+WxcYSus9u>XUH?W6=A>AjJn{`C(Qu=wGJ;fJJOGEURozNn^oAL9Nu zdODG%;gh0)xbkNeaTXcvzc}y8!*R$)lXC{=N{Id=7-GUgO57~u(NDWmpEUoj0i2Sh zDVE6GE7})GFRM)br@jy~`v1`KW1;@b>X(N9KXLH?H#YzOFJdz%W)1&ykAXd8`vNlJ zW4)w5lHcH8iu|`a9TRdk*uRvGflw_Gzy9+toD$tfXxaZfm5BTw^+TizVX%7sD_{B@ z?#urZgRwf`{8*k>T4KVG|BJ4&gpP{hA0_|E`9#4;#m~aQNXx;(K*P=y5Am-@A;sKm6PHA5|nJrG`gncN9ivxWPJxwpTKr=@TH5H2TmC z`33q#(&u-P6#yYW$O7d*dsIMOmBKr)4&w#9L!90X!`7k~+Sa}dLkmm2_M$i-* z{lcFR_W1C3V1@MB_2TfDWW?h$6NL=Sx+(Q6Ls_?PMyTB!y7HdcYATRs;-AGkNO4{)6xCV)@ zaq*cS^MC!ufKxi56@aez6(67ZFKQG-Toj3didJ7+2oe+iD!crfjPxiZM*XvgM(Ov^ zm}E+Pk_Dvg_c(`iQ~$9&I3;89|2wP{#0gpJbZe+rQdTtdi`tP6`53AkVNP%_Uzve& zK3^zJW%BnniyePyDa@Yr6?R~|>jZGb5gz_Yw;fNGG)zyTxxy4I7Ysa)29=kW?XAx@ zbnQ0nJ3qTvje-dKljLhDXlY9;N`^4rU=BXxPAi`BuG9+pGLfc#?GRMi>BWY9_pwNI zXQ2$KOjbta=aZSs*U`+SCa;Ahqe@-9xj_C3KWzT>l`X8LV$mwR#|Rt%ER*U6ILZ5v zlYA!$NZ<3jytsTk(RP73)y?O{`mfs3EGObX`y3BsMMk2)dvd3&H2v6CRJyaSdm)CDFCwE+ZNh8y3;QqQK25}#90r!8B6KmItSnfb?Kn)Ia^TzR ztSgJip%SHZUVM4uHQw22zTo{VFN=f}2I0FKC=@FWjgE|eU~?I(pdu6WHw%Y5`AWse z&hxoSQNpUE+>3Mt`KRO`pe3lo5vZgq45K1DSE**=FgYP|?_yxSVStr@!fcQeFwRIuGMFGo2cZY+rLdO!v7 z8L*P{)KqW8B0vbL50-(NIwL-Q|Q)PTo5`m!mQ^H~Oa;o}45&fa8tel+9roXDPvXdNrM7F)%nO%i|FOpV+ zy$${1v-|_c>I0de#|aEkx9{zMUHTj+Tj}Fj(q<&pY4RkYfx3jv+?^OR2&M+V_2hV= zExWT)O03VK8l+&*=C}P^%~MEEkg{3lvd|!AbII=)o>S~z$?JMdXbb(*+f7+nbJ@Vr6HLRbh%BbOL?_GWbT$h3D+yh(PAdM%`w$z=!~KUI$3VtXa>_!x0^k zwz*+!asJ>dyKOaF0R6lpZ|7Z9Sja|&r3t6oj?yDOIxsP69%%ulbue(TUT@I1gMfbx zGR9+GrKBa!g$43{jdMwKSVy0-h!m+O=E-}+sWgNzTK^+fva9r2`^g1|+nq|%DST%~Y zRfI}BnlvxA3FAFozSLm)*^9B}fKHWlrOxG{{g=X0ga8Ins~E*0e1e2Atgo~LFOiFV z!-%D&jrGvbNDU?=w9n@m@!N_H4)83N2I=QRHdtCwr})V?L_bs~c<<79R#{F! zaAeTs*=)`p;V9y@`^s;wI7!cWnO{GXr_@Gch|f*NfWzA0!}yoEUf zU^enizS>KYF$HkbbN(u(Nu_+olhZxf=}<6u&P7izM<^}M58B9nJXu^T_!M|V|K=W* zb$Y?$GE&mfm5_D6VRD$=eSP@p&dG*?kpgu;If>T1EK+M98A2yt>i|lQir-tSvU|S> zc-(eTgoPPOWUCPbqh3~VGEb7e!MoYFmfZe6+&UbWVXL)A$GdbWGwF? zI#(L8Sf-QtC7`#WMg#aBh3^Mo*6WTM z5pKTO$SK7)a}!#@d%5u(y}ca57wUH$dFXSotD@n=g;f@AY$>( zB8Y#fo}8XkEY)RTdhX_XDCl>X1MJLn*0G2}Glw|sWN(f(EA4K*V{&?vPzhT7dgp|& z%{L!Be;0DNDKaO7i+l74_`fD&hJ{Arp3|_fDb9}-_(q6Z@B;+=k}pnnwsaSpOx~u? zJn-aD*+bLR>ZyI7A5eqP(^5{Y#;z`S^+V5MG@uZSs|>g*^Kyk2b9djXcAFsFyV>mX zIPoaF)*a5zFX*zlOG|3D97gdn5^+~&^S1hS$DMhgcI0v}Z);pvdb89z!nU~wX5oAG zpibzqi)vGu^u5>UblhwNEDRNuabNsNRLwpUMXYF3$I}bcXQilf#a&{(t#cXBl$jkN zawoCgsM%;x`dP%F%NYH9zERbG$0Cy5`S7!Yvs%~aHeQP--3+t|U+e9dUzy5(eX(0i z{p_R^y-mu(!`(53YLDMG(yH)r}aizD0unAK?eT6>#)UJjRzW)x+0d{cZ{i3}Ic3}R8x9nD*tnJc9qa&Sfn5fxY)Jy`s`3k@Q z{vR<z&S6pC0f!2Qb5PAbL z(c73=HRxs_PeZ-kCgT9&B0pVC?mMJnH6=~YL9`R+-bU3O9c$wTS)2c00bth%B`?kk zfwR>@UrSL+3esmUo-A$7fcD^FW8+<{wq}5mx{mV>2p66-l#O(y1I6hYyla8?8TTE~ zL8sOeT^Ct5U0NEM_9M61^c)L*mA8VPW=|e%-3%)i@;ZCNx4S(voq_p&OyARYXl~vM417KZ@jtWfTY7y5#o0xCl8ugZMmde)qp-)ROlqU}u5h z6U2q-C-f&WFi~S@NEN^E6Z5;Lv1uc9Ugzd5o^$owCDoLgPpodKZ1#3Zgs97KUM%@- z!DUSPp5ig*u;&^Lc3p>s*%G6wHF}+U)yfIO?U+_rO5mnKOFn~5ENb$6XE!Jgf%ZM; zy8?XPfiT&H7?RXXx<<&bq+T)>ks>A+>`u}F&LUnew){Pv6#AM!1oRsmJa82B_ZXL< zoad)2={-5;(kma4ESSq$W`D6%4BNh4-=?5}D~yu~)yBCo)r!rdvdRxo&!)qOYJ|P`P{LZgk|8e>d5K0^aQ3wV{v#$sgJLsy1DZes@LXBiv;+ zd7%D9B?GBRFIdDf=^u`RU8{5=^cEVGCax#l7StW^ddVG*?!@l4H@ey@O)2&x47fXNByXsIQ- zGW~&wZN1R<675x7!0RD=n|pY-=Dj~n?OLgn6zb)y?_?aCi3MW-Tk++Q)jZ=Ev8_#w z_i8g_*Y5nEhP>MFa4e1`L9@aii*doc_K>Xi)SdMEsq1+3t4;G|NJe2E=H%&-?ke7M z!OK-x#Dbm3BvyJRElR972KQ8_B$?kAM?7MVa#d)7wrmROd~9yh8Qsu4v~i;M;#a=c z@AxmJ-mBZM`r~|pdxtNMB5%-z=8Hvy@BcKQcUSoPQ`cgtR@VA_LD%kdeKyV2vCIM~ zuRna5xFxbR-N=#8C6`P5EXrZE*Z*Set-`X5+OAyzDUp)yP`W`nB~@CwLAtx)K|n%M zBqb#j>F$T_?(XichyJJU`+w`(T3c%!hZ}B!-uIl>HO4u{-R=Z;To8I5=8QX6gu?Nq znCk3mXE$7cXL4BoDt|s-Fu4y5wSp=;+(bdoK4p|M5zf8g#YuZI>WID7rGP7Tp9-G2>LNbXcdm;i{f}>z=!56O6v7OdX0(euQ~$`xdxhTh zqPO?+A+5Co11U*YR%B zdqalHyUp7OzA7MjAsr}k;TYeeDWY7fpZjixq*G6;eO6q|HfE}slAp!W8{~u$z$aBz z5fB8ZeBZMX>ZN_EzF#Qy6gHIzeZhq4Qe2N}O9lx8 zTXzG!2_gTs)z#KRw{SnfXFWhoke8F}V9FCOQIPSeZ-hVl?Jr6;$k}y|SUDPARgp(a z=)bSM=(VyxI^6j2_lp2->NWUNd(NjbLI`gZi^PS2JR@NySD$nPfFYVWCe>hGCC>YQ z4h$g)7nLvR#jv;Ww*N+FG;@6Z9{0m%WdgBP%2bY#SRAv3x*LouwLiP5R1KB3_8{c~b_ z5Y$3;v8$65pnFxbd_o^sL82DOI17kXUJ{ zOaIBrgp0_1dE6C2f53aoc3#waEunvezF~Yk^NQn4Q5A#%YBQu^bRn zB>1qIj3fE)$v66#@GSW9_DI4&Gm*?{R;(kdq7$X?Y2dc%N335HMl5X0&y!!vW&M}s zuznpJ+(Jtk&n;=O51da|EC9)4ju)lOSvgL(%N)On65uDquezGVB5s}Jznu0n;)zi)ycB`1n>?}H=fkSkI zfZ&0M(~BnvTKI|?EL;qnzgHtu6JO|5nhT?VW&zgBtn#t3@g}@SZ72Oswki$`HN4G% zLb+BajPQtnc^-a#yqgenL4NL#fza@$8?1Jz5A3f3x;gKM`+Zr)vIITWd~Xug#on@4 z!o~X_5rqD-AYw}B|EAcZjNR))lYN0^KpPV5bAJyyD{jFMxIFs~i~Rt3@E$YzKbTgMs^ZXARQTX$H-{8G4#(a$#<9Nx%9-S-l@He4iuW_$B>x zFla|O-qQ_X`?9Z#4--p9k*wqpqVU)!pU%77?dKJuQ1#4iU_yqVn1#htFLOfRj)8LX zi{>lD6LByvzzCxxZ^faH7mi|NRlmmAuQ?nlBzZ8W0dkPV{SQGAize8y5}M{QMA~w)z{O8lDg$P zQ)=|p6nx5)11_hP9CirOib*v-?E$;P`Zw#eEGJ^kEtrfSKeCGl$w-$`c^GUbZv+Us z+-&K=3O@CEY!-f4N4mn6dB%0my^5J$|3wt2;}u;PBy$b>FDac%9 zrD_a;1ILSA=ky3W-*1^=%?@y=V9`YU;6_ebm0zsG^k&x&J9wqO05cEfh4EuO@wmAe zB69@5EedkNuiuWGHtx8hH4alX)~{^WF)i8fy}EBPXv#9pK5+baxjzRWVy%0Gz>}l6 zxVVG)Dg=zg*!aWEDQR{Mwg*p~vbndGEw^8>%^x>n#--6YnRC1Emshv=&;OWN#(Ulw z*9~Qzvc%=0j#hk=eQ)@5(QkE*pZqE%-;DNPL!aZ+k3zL~=DYKjM$xBQ%XziIiuDxr zw_C0>qR;U>P!!ysde1WuP8ih;Um)lrN4fEQR(NR!lkgdJ<>BfTL6Z}`@tiP{4U3Aj z9qkE1^S}0x^mBLnD+R#?qRS02e(+IAaW(Bc;TY!_`h_c^42eDz(qEs?@0D8WrdLZl z71AwPQ|A7zH8Rzz!Z>}|?|!0IKMe{G!>r+mN__MmL8`JXto?o)<)$~!V-98vWQfIq zYDgO8;7PaU9Lj)@Pp6-BmBocKJ?+Pr#q4B{CXwRX9c-lm%KY#A7whCzctZZ?iR+7> z46-w?F}kpsFI>$np<1BUuQQZ^tDPw740>z3lEkDSsfwFCo}?xFc0w&tj--DBlbHqP zJ_mBi677k@sV%HC>XeWE$D@38jMCxdWqrbR6GOMT9^=!KdB!^J$BV;kknmcQ(V~J< zIlW2&X6jcjo{rjc_ua{eMROE^hXEh`S;IRxBLS_f=J6*5RP)ZwavAE6LvXY85F)*K zIg{w>CE)imDm7vxPA_dA<7o8TV$?l3y1y^Q>7W160>TLDiTr3}n_DOe;h&RKG!C+% z8iMEUwC^cRHcdM~I1(5oN?ZTD)zcaVM{+}GzWt)#!e9NgEp$)&cXsz&?FN6~%`Vl8 zS-Y!?poi;dAuLN)JNkvMVDKAS!zsc7)y2mx-rYdH(MqtNYcyJYdx12f_&`o(Yp+5! zr3I~@Y1|nf(5!cB-B1qns;zUk_-2RfBeQc+P+)egU40;^-W(;Y@2%hMrq}2?JXTpA zAH?7@0X!k|j%=Xqet-|GX_o19$nauZozeVRV{`D5`K6;;aa2fTDZPo`=oT#8^bzTY=APfJL~SjpNhve`J#(a5dFUd|qEeiwP+ zW<6fMT}mIkE$e?GLGix1kaf&_+yur|Q+L{^#NAz8{I%qd{#`ot0$ac@)JTdkcavsv zI#&IvB0H_RfXtPpyw}K9)(J%M%`r`$4_>IKAHZ?!up#1kn(V6kE4(*K@^2eK&Cav3 zY*@OGU)uLLmz%CMuvQrWaT=W6msZ(2nn4UN!~aD3GtN6-ylr5&@&EM}YA5Poz5BvI zOVvd#cS>H$W5S$Oui5FUBQF~%t?}S@&fk$nUs;*EhXQ6QWZscMt-WP|(2I{#IPs$5ryhp;4uv)6d%yz`w(&QA?3C8T! zlV-l(z)+HqIx)eeY$D`MrV}gnB*zNoRF!zWkaV>UOy|b9FS~)_x>$6NzWP3{|$CMvC1k zSve3PmFm|l)5W;Ax87FNYqHeP_R`Ls8f2DZ^pYFA_O`EeJ{E;6wTwcpYF8~%2T7)4 zIZK_M!Qf%Mwz86rKFL5QB<${5e_4Sxsman(JmmdidP^S5swfG%ht;7j`EE$QWE zpVjM{*MHbHn!didh!4qXm-TTd8S=D(82OH2MMR=)UVTa*-+US^2{BF4ud0%982S zvL*|7tsk|QQnwZ>ZwSUox!^hc!nY~V`NdOnAQ+H{Vsne&U0W7D^A8>JOMFH5h3N6q zZ>$!NSkx##v!7508r1|@y#}*OvQw{y%azkC*ZJoSx;xa_;|K(&OiyC1WzHU^Lt15J z?%UOq1M*-K26w4`e?2kX7r2*QsNbL(s)+4QCCg1Zb*{s`hoa4DSU4o0Zs&q18+p3W z?%TC+eJ4TjvUZheG|c+191(}u3EeH+Q`j>R*Hi7`C|e~v%iZ15*QMtgtL5|cHasM7 z5Z9@ys3@m-5opN=?!VT&z5`gSLFj0IxbCTps&rgByEYJ+wiSs{r{TP~9Nx+s)F<4H z?EmTH;^!q!gv1sj)ZqoMui+@m|80#^b(E3MMzvk^dwEu6Kkatyn~~A%RAFCpwEzsa zsOa=?;xv!meI6&YPP8;0v#VD;rBdx)J2sAvQ!Gh+c9pxrq-59NW!&AhDETR zmjD9vY!3*O+B5dw!+u%FI5MYjkQdXrr4*>&ZRxGlMny(b2zV|9^uETl(+vCQ3%fuR zk|lz7P8!W@|b>O)-mTwxV`Jfrbhc2m$zz5ToS1WmKra@r*N5i#F1*r3yRo0V*`nJ)dOMg4a1LI)MrMnIr7 zmBHzK>q7vFhgxK6)B6+aPwBizw7+vu8vk92C@WSfa!1M?x{$NgPzeDs5{C2K>prdh-xdJpu z1odDlob4#G1@Hc`+0thalt-xDc58bs+dpWhV_8~UzQ6NpDlOfFEe#WhS%uXeQdpeL0Dz+ zfM{fWvX%Jf&)TOfw}=!asS$E6tIb)=Y-}NRjz-SZbaZIu+=8ts2KB*^BkzU1SkR?^beioLCfi)&+HZ>L_X7@!8#b)i^-vBZ1%M7uwOgk?vgULn6_ zhlPi!>)t58n$sNpa-(RVA((g*jR2v0e{mmt#(8h_(*c8^f5Hm|iA-J|h9@N>*kn7| z@M{J2`~~cz+bHK%cw!)OgE|rVDaD)x4=Bj>b5GOHRI{=Bbh;76EkQecT7Q^boKH=| z(#aw;wB~#|^6gxM;8fI2l!)s7N+&|f8p^%HLX!7JeJK+#ADKeeUBKi>pH=V^6RJg_ zJoMc=e#}DFB>fuIVXa%D5Z{NF386wzFsP@!XeoV96~zsBA-oFOi2_?e`^i7RRQy|z z_wrx~+kWQ@CWZ6QpUQr}l^z0i2+O(x!Z2;lKSP$O3D$hP-m)pFdv@YAjP&N)0BzCg25OXh+m+)mFAiGKcmg00f{k`{T6+nyeOCQL=ghcYZR+9Kf2<&g%Ei@9y>f;b>GM zrf(+%DlHB?*X5aj*eI^5y!E@7|?Tg{J<^%b&>8k^n_}RF8-@h0J|> z795>@_O=Kh)gb#PvNbl6>TGqn5qVNIZqvl{A6Sv)ax#pvk`S5pV3!776}Eh z7c^z38U6w^0E&;>;q&urO#$D9AYYQ7)MdUm3!7ZsHMUbdl|QgmNZ-rK&Q6zVmqWS- zOaaA(Cl`VKvn^2a!li2UgBb)4-Bpc69{taT<9WxC)_O2UQ1|v)=7;t>t|!`k0y)7Z zU+xBBJBMRn+svxhm_X1oYjR+rtcs`h1&!7OR3rx!hGVLJxbdr5wZ1#6uakg}z^9H?|_*BDi(^+Cv%fv&paMo*we^nqIZX ze=n_d8H!BsovXE|o6_;sAcv>eI0d768#VRv^Ct5b(H{`AV+E&P{(L*VKZNk-??4JWyl@W2gA9ix zs_}G;%DarFfA;-N^9c*{XFycVe6jDVg+jU$UuuZg%>ctp}~x=|xTQyZMd_t)@a zo2$?-j;B_avdzH&X%_0!qxNWiRVWBK>RY>1uF6oZyxC3OcL51>lyp&M(*&*6U7)jO^R@l*?dy%LNqu!E|<^H_X(u ztjl+Z;4+pqyA|>-^XX@G#hc7*Y#?2zE%a^1Rvs<;&-V2c^xVNxQe2#xi7EU>xTPl0 zk3!h56PF2Ro5fyx89OaET)zg?kpFxlXs(j}h}(SpaT(FKjpAGs{>Ms3P{G150R#@9 zV)m*f)rKq0{f{`?No!lzRdB+3hXClN$@=YMog>uTnTa%m8^~r5Y3r+P2byBD(d>S~R9B&ENIQ(c$auTHpZ8G6X^V4UQdqFct{rD8##~%nquggu zPxIEjB1cqIn-^oBKD|c1u-H)5k(Z*`88@eqqInLMo%UH292mkfK|%GwyPnij^02To zu$d+CRhC4?)A(dsS06U4`6^o6P?qP1ja4Ib(|KQ%e9CLXJ8s}gDCT0!6?Jncnd6yk z4i09Uuaq4j*i-7ScTO>LSVHo zu&Z>n%=Z7$0@R#1p&VTWTXG8WvU=du*(7m4smTBNA(3a0^w73RrrT@Tukw7PH))3N z>hE~NR3=VrdcstN3W=)ZwGqzw9~<2!-Hf3_-My)gav#B-a7&IS43?WH1ZlLYaUcN! zbgd@dGrq)VRo~s`ij+T+_Ny#dX8TTbYsq`_5JKeBw+Ucyq7BuyONIWimNe_zB@fCX zbsu%Kv`$Cz(C9gGwnpl^M~nLV{}#x8_@FOXPu5t&faV!!PX${PthQP)EXn+HdonAl zdzScJ$q28lVN4~l*f%)aX&p{1TY1)yKD$T2lupd~>K)lL%ab$yte z#{q_%HJ~y+R_wK`)97pP=@Vy7jgH_3H2s5AHUK@%SoMZOC0#xR43P6e)zSO0$MM_t z12W`wNgWb@s=obtTfgOd+|rAtD~C;>|Ch;>MC@d`r2WoidU~>c{oLiL*SU#p$$<`p zX7)Cxs{ONt%dt;HJT9vnfst~y1@%3bZohRgJ$`3{dAL{wq~mcJ%wluy9`(HmiA^ISBO zxGK`QKkc8+L(>RX@uKcqp6YYjnxO(REtyVTef`8@`SH%`cp{cjYfj9qsq1ZYT1>(1 ziLQ3@{>Zq-<9xz!o!9;ncoMNarp~(nnp-BjQ+MlPRZ#f1)a~x^o|DUclU67w>Auyv z-c6oq$F?{*AfS1{@2t=`9R7J}TqW+&4(Hx~+R{($KLu<(+XtEuQ-R>$5R!q$WMX2# zf;S3O>2d18-f+-o$;+B$k>vl$veE9f06PUfZM*}Ym-0;bG+nh$eZ9l!{#1Ffh0QNY zx~GHq=G4@K+n{m58cS_;or1zB9)VV@`cOuX2DAdvGLA% z*1XmWrK`RTWv9R2yuqgMdnFq2=<%+cYG!IejQHb@Qhq*Te}69nBRxF>W2Nlq=lFxZK1?1Q*U*{Qb3q2qN zf|c7$RXwz z&E&aq#jzv?Q0|PA&XiWBrXPW$!N&l4v^!<>Lq_y#cA{U}@-*~%gNU<>ar@{{dw^6> zRYAe*>DxJb?FNf2T5MQ>**R_WyVWkRTXBv%iIL_2w!-(1`QUDHK31mD89>5Mp zU>toTE2s;OL(|P`bmg{a>*qag3t+czokGLh4HW}t8W(1WT(fI5S~TTb3P>1Z*LC;-U7?9d#U3WZSek09V zHHM5iv`Y;E#n13}1a`8v?eCElQ*j6~Rz`F0TFC7dmcpkS>ClgnFSAMt&bi;dqyqFZ zJCq>bU0S;9yM9_38rkjRx*7Bg(`Ln_*=#5d%3oG&CSpHnrkRa{6T3=hGuy*Y#ew=~ z-@N3(=u!rgQ2b%EA`#$pY$sVWo5F_G6r7$z1-$-1=$Ha$%eAbYe0*e!+dD%83en2p z#Sq2O_V!88TqyBM?5`}lD8Zy;BJom1EP*HMF90r+E!Mx1b|JhRlE3p;yfGSxEiBudlzLWJcF6hD2{v~;c#VB1IQkh zWvF%ANE1kH$S4?ixa^13;RtmN>L>5JIOLYhJ3ixqhlxdHG@pI7??mo5k5ZJE4`d5< z-2%vx@pisvpm%NS#<27kzw){}tW15=XXb1IYH3K3o(~DU##m7gvdErxSeZju)SEWcqYko z8tC4fzPM>ylBe%lRoTjpT&44JV&_7_2^T`n?+rcRUF?@X4-9nfps&;UOat^WmB&g6sIET) zdOnxWU62nk`42YOrOnM}N*TY?DB5Yni+s7~et=$q(q17kXro$!!Tj36?;SF0GAHq% z<;d4r(XcG!LJfC{TM?gIP7Nf8Bi5!uF#r0t0rPOO^L)!T zb69RXOj_rPT(8WO*PGvanl+i%a>JHlx%TT<151P(yZ}B#`uF^Li@y}>N(ky3&wn}2 z@VT6|T^b7-iaJ?M2XYrvJt?@&zH0Pr_rf~&NGV0P>0f#s!gCr-j9Q$KY7nzV2FFc; zL(q*rZaX_eL`zu9JIk@5b4A8q?fTo$iaNapuz+nHFRu@ZkfY1chW#v*8;B=o5Kec@ zVAcKbve67EJNiOzyz8sF3R1nb!AS|6S)a!}`C!otGho&LC*9@+)0>xAB<>V#=SIUZ za-=0sTnQ zfRWZV&&0yw3-HGS*<{2t1-{$T(U#HlRllYzD!=ssQ#RIjn#q|E&`mlONB|MXm*;oB zLAqt~KwI-Q=&nW*cOw-#x;sPfc&0jD$HYshzucCLjalowYJFCJ7`XIQxVb+gCh?GQ zM0r67)_K#M-`jI$!!JtW1wMObs*RLi>$|&1&pLSMtyl z=35RxuZ!l-=G5}wdd3M9floHPWWQchYo;4+XU5j<(TsP0_8tp}Ke2C#=?R4ski$Cs z2X@xWV{LpFzrB|Z7kiH*vSQaPmReBL z2V4cO(4ydj#!(;a^=#$;gW`d;g>@?^-19dxhvTWkqe;eFMl(PR)=E#Is!Uhk-bH6< zZX%5M2lKxaVRg%|vv5GGAn}m&SoRJL>{t%pMsP>_wYhFk>vL-F+Ry0li)FqxJiN|v znrh)=p$C>^>ioyIfcvNQzUZd#ITlnO3bg8sbUwyD?p0rehGk{7-RdFFCY`L23-5nZ zhwG$1d*wpNF&E7j-@jO@^rMvO#UmrYT@YK~fL$;Y1o^%#`zJE>pK&Q?QG6bE?VZ z^nfoQcd=VB!2Y3x%INHc(@h%h8|oh^Q%SwYpHZ3)#>d9r(0CHJ7rrVH1F=5m29Hf5 zEEKj%L#uK60rsr_(EvzLyi0_nPkyn?^VphWh@ z^AP-~sca6G4%Ku(TQt?gQmO!IUSwtF z?~$C-YOG9`Wtj&V=O>_NH2CpR=YC7$6A7N%BNTi}ZYEWOva zddIKgedYAUTgFZ2;vB!~CBjGBCeR&PE$+H9CEYunuZ+%v7;3WP){1b>Bs?0QPBCZt zRuio-=z~-o4{y9MtiNW3ZMKL*gai9iv#u5y_HG2x1lmu2pDpTh>(Z$gcz86e8jSmk zkx|&3lQ&($8LWZbWscR%X;n_=N67bO3(6doCZJQHLsa@3PRpsNS*^b&#E<&eMMVmN zB0%~L@Qr8RWCx78gHl|&lBgWW+YqMy>gxs9WJy;uDXJ&%Qu6a3=4!BW%ZG-rtNfBF z&KlUSphekWiFxY7tzZk!d4u>$4t#@;1X+RKR0{E;cLX*3&Iqa~|G+>3q4?-yE$%d= z+u234QD3j0Hs3bTY+iMj!=zASy|>9r3dKvqR2g?-@H zt35sacGDRmZar!os?$$dcJH~mu(OXCAM;r*{h&%kuT9wo4Hd@+c3F@2Yq_tUl%E9W z#Ej5RyyfI#2!zFDhiLw;;k@eLS5lE6TXUHBLjGMp)t=wpsti8khT+5$ty9qa-oaE# zDyh-O$Kcm@BS^m5;@EYkwXdG7T~e@xq=pyd*J_zrqLZns@#ppD6pyIzB~JV?$3c7e zWqM4E>dbe}@GJ@8Yf3xMto)6q(QSUn2#>1>8O4sh!9d^+V~o*j{znjDz*3iRVmMT$a21%dlJUTGqgB?bwkzN z9wIdCJw;Y4BM$WAAX;R6C8b8m&d(EFhBlJ{4o!E^#uAlwE_p4bqtZUr_XE9>y(p(J zr{~^mMrb{3h2BuYV^eEVNUo(}O*eOHL_aOjX6Cf-c(KfuX_Pyo!Y7;WTZQG?kGhVV zZSWzSf6wIVT~JeylfxJV`@66A9=3Q*zz+3iv z&^Ii*^f$U9Vk-I0SmYx|VtK708Q--1GJ&q^&-N!hET`h%dDy3~x77-gQ1lWCTXHlV zx+60hY%Uji$sg)Py&gZF%#+u4ZKk-dVyZ{SSI|b1%pk+p&af@;mND1i8N3G ze-ZP09?0jhwQ2ZC)?~D)vqH@e;$L)Dd#SZDRCR5lM`c{&665Tl)raa>6Dz*tLw)em zA)&W{L-Ko6Z_>pkVE4W|lBi0>@w9qKa$`ycmc!aN7l-w+-{akv6oP^1FFh!phpROjo$dD^oB)s!0jE|6V6r6YtKANacpVmh2hcKR3H3RH9J z!yZyq0yBV}w88u9UjqcpPibxInc(bvsR+}>@PVyIB3a8Fi?^^`Fo79BJ)Y$A ztvm1jUlVfB5jBR!QKjx%TyY{PT+aDgS0(4Gj!mrK$DB-|`h*L$0%8 zcL8yptP@`cbs4c-WL6-%Bjp?fpH1x;cm6T}pQ87o41hRuo;w>^dD)}h}^ELPPvkyqA!(AiBg64?OBO3a?#Uiv??C=-fzNZ2~*# zFE-aBy%$frlh2cAdmtHz*4#VT_PwGE(=qe{-kymzBQIE--)Za9dyfu~cAm{#FM-u4hErgQ1(#HYTpSc; zYyD}-d0`{ksaY3*7vhZr(GY#4ZWk{j=s~*sZZgQ*oQ;lK{cM!7e}9)#3O?~=O4Q05 zD{qpr=j`c*CHDx4h0CiJ=3G6Ck+SGoc&P}VbPR=q;4^C25!3hk?jO^w_6D@Ozi;(9}m>j*qn?7HtmVwraoWM z?~Jn9wHU&cScG%$eR)M@hi+p?(C+6@rWbm@VkZzc2`Hs`ZtEUy*Q6UmH&M^BFPqsF zoE7ka0qZyrOC-*J04KqJ0M15*WBm~QKUKef1}D#jtPFP#>-Cb@-uzi4!B>!wsrym3 zdc9-ey%b{Ge&OmYNBY^EchY_3a{f61bg}X-G+R#|y2nbf0B`%NIR6DNrxzi$Q!A`vCcJ$?MobW* zy*OX>{-c8W_ov-*Z;Jc6A4WGopb>AQUMbt~t>8fWC^pt;T%c==A~ljL+kh%@7nM>G zp=Va1#H`_mGAJI(RTIsa}dbRx$JtOT^)stW)N78$w>%Eu8qCS)$-Stgn zZt0qw+p&1QZxZqG!^P`F`#F#=hxeQ>KYlMR{t5h8$X5;$8WA_9z)bpIXcKXA;=j!1 z!L9j;2JoL_6VC?*yra|Axw&@dnu0=)ox|RL{9gO@Io~XXjb^>coMvtMiY=2t@xk|{ zwBI?ujRODLSx%M3?*pGgD_8zhpU-Zo>O7Zvu=1i3xLR6CsuxMgjc>0(Y`RD{%{-c- z>GQdrhgl)B+$dkk@}+-B#tli!Y1J`o_MNa`JCW!1>bTVwZJ?J+{q@f5jkUO)z-Uc8 zaPAOD5?0{NS#%6mJdK)Wq1z}>eTx>EA>CkM_}P{sTr!;P_9q84=r#Bhws%xI1A89) zydsA>tIr+QAz(|(IeCeu8s8-?K|4H%9VYs}*h z>;ILXuqmKr1*KD^wu1&0rb`{zGeLAe(u=3kWSWXnYQ^xtK-S%_-61~iVb^@oZ)D^2 z*A!GW^+L>(R{i#C?O65`go9DiGGq7u%)71301lCtCAkb>qORPN0Kb=%?j&t?2*O4! zpDyoK`p<=7&fo~|v6;{a$ePymL;y3a-+k^XcNJd_(sw5x_%CU>^JT55&gJ1sduq}N z)zF2gWC4n1ZlkSzdt)u?t+%s-NhpZmE8@Lx1=Nx5VFP8=?*uGgsB-S2(>RPTzH3jZ zt-1wA3H$Ee9>kN>1x4W(E`|~COP1G%aZcct`)*eXyL)JR1*aC8oavs zp$zn205dw51s(%yCKx{u-ElWI2vMcAKY6V|H+OhUIXiP_00 zcI9EFPFCjfxX!56@D_FxzD652J?k(YtSw7qTj zWH)nO!0$4lKvWu+jxP6QI-mKGM>Ic9aQmKrcxUnK8hZ1Sd@04N{jqBPIl^=WcM6fT zGLy6s?qQ&L68$8q)lvCcghGjXF^631PC8bw4_Z4MDX2(F7gvb*Ogo-cI%s-Q>UQc9 zXt>-lboG&O`;M+z8vkp<>i2_PY{nrLoRRRKaWngXY%&^Mw6w7?#Sd8?6Thn~-YS1_ zycN?D!PpSYFK3dZf4++yqo;8axP8|$4r%qryx!{YWV$M1>Igl`3s=vWDf7Aw@LT*J zE#RgQ)%GqTH0qnFN;?-;-aGWDl*Wslsd!y0@QO832#HGsY;A~-1dmD@ae^R^UWj7+ zfK5b1_%>9|S6-v&H-Hr1zTiZg&i+C?3Ds&0h(-O?&AiX+p9Wnz zH1qiO;@;X;(nDIeq;+-znq?sh>4L5sVuULWXzgNLMn_@Le&2r_^Ap#biOMj(5(WY! zj5HBMAybIIEHV#CA{+0&bg~Yp(Si;k5_I%og488IXnx*64{6blqaO#Wbgo0>f6@_R z%mx-C)U>b(hEj~~onl~zxUI>K0=bsk;$o@WYa`I@z{*7DwG+X$i zFQjf$-tR|j;2iC?f24SsVO*k z+g%Xt$Fl9^J}+~-{T=|L!oDl`A8uZGTlf1n>Chux#tLFQ5Gtm5W>xyw}KBpL;%6erPR1a0Ojpc6nv zP*5TgS>9oY0?JXo#=P4VZ1p%$w6zrv_a3o$9hBN|`WUmTV7)(-ecUe$I}*=4LPQqZ z0v|k&e-M8u&?lwTjA$lC8s?_Y(OXj9@urf!0Qnn?R>ozhL>)J?(vv%$>$!05_vLZV zHFdv1G=a6CzPivH$uDk9YfOWk9;-Vv;k9)R`|h_W(0ef~rO2zPw zp|}svuQp^SWW(MH;WPe9dbW9erRCH}*%jbmlBoT!lVK&*28xFS88w|jhO|JE%`{Lq zKmYL)%+vyIS7#wBZ0sd1d&;9dzpASy{Z$CUOw+i)y*bi2dcp7-2!&&ksCX^Sp>c5& zz_y=+QP9ppPGGSVvb+@we)k9G(^$wl)*-f2x+?%jC|>q#`3JyBl8Zd|I}&w6YWPg5 z{FYJ#5ar%$Ym;SxUL|RwYFZyS82F{srHM-pT7In*)EtH2Ln2)OMioHMO!NL32-l0t zt8G4Xi~CK!W;-h{2X;hnPY=}b*l3Ac_5|Zd&`g`#S!F-p3@@_J@BZR)*7^xqvE%k$ z0-MiuH@SSq;PFl9SXVt6G5|(fu%6h`p&pGs=f!bv%S0ycN`&4V3tgCo8gyNEk^ow| zEVC%jNK=80g@w${54#_R{qM&={f9jK-_Ftw`+tA`$A1pOu{tip{ckI1=rY{@(_Q-i zu|L$w(B#*0ljm!Gey#tO34(w4CxYMN&#W!2*lF;9#s{<)uFlBFcquJ1maS`*bH%is z=$P+uZvh*jP?QWhYyS&|-Z#VO5Kll}5Wt%xOSuI*765Jq#eY{TZJG1AS8E0~)(Ohy zk9>T`r-udS^9`P-17JMr>FZ%fG_w1It)aI(%G!(~LhzpuI0-lo{^ehUTwlIM0{9xG z7SKsDy?_7y4Pef5=MeGRBZFp>)s0a(BPA7OyQNZnKqz%%?4cVJfbeu?qP?cV@yt|w z&peo8C6S%z85x;>@*sI1rwLOED1C3zn`^FxyufB8Az3q*G@0nMu0e7+rdRllm6hVo zx}eSdjz7^P5cj{o*y)pW^3C*@=1Qq-FC_*+jEq;<@)CNNE5W}3iUNo>cM9REVymT* zJU0MK3t$kEk>T{m5pI<%Wo7f8m49MAYy?!NAP0>cN7Wj~kOGuJ=0vwmx-$KkvK$L7nY6-{iN0@(Ku5Of3i zTl2lFNzlgkc2{5o)O1WW2G7>IE%>b+?A686VeEHHr3*TCL<)-7A#}faLmP&%K{&fC z%)wA+{G`mb{5%w6!(zJ7h?{F1Vr)h#yw5=RxUor+^u}slljF(Sfr101?UXfCesY-o zm0+^LxZWGe~yxjhvb8-Qwh1N1P5{Rfs#mQW#{(vId6d(DJZ>@u%zSnNf z_Dw;lAma9%_Yq9uAKtI+ZooP^0DWSZom|Io&dbhfU1YI|2!gd~7wk&aCuX6U&{z%X?LR&)J(;nYEnh=tIJ(1|Zdz7xK9f;O^`4*Nd?>bx2X^)=RxH!BGb`vlvAjKqZh zhqkwls=8~}e-!~qQMy4CkVd*uDFG?z?(Xg`r9(hk>F(~3?(Ri*cP-#deBO7zdyn5f zW1MluInO@}^%<-9e&@XBeP7q-I!Fih*Xi4>(MW$;;nq2D9JX3b+m@my5lS#C;($#I zat%~Cs(Vun92e6QV1Y7Amq_d2YjjDI9Kd&$>gqIJ--$|2(B{;8lKZ>E!E%vFQICp> zYW@n0s_PstT6#I7)plmBGdG=$Ou^bv`?DMUaGc*Dan~rebqJjCN(>uZ^y+~mtU>gR z^sEo6(mSJzcV8-0GrXu73yt2^C?*j|ABn|aVq!}X*>0ZkRlp|U==TY|RAD8{L%%@C zurk?HsR&vxrvPF;zp!h{=3zGP}YFBOxdFzy6A2O z8K!D(e5V22J-78q=MC3a%60j{zP9RfVRs_7&k%qyZl=4`!GU+Okkz(d8necyfv{Z> zW}28gCr(u8&7S-H4c(ivEl@^IF(hRb=i6>~66p(F8=&q?6e%~>-Y#_?j+jXT5U1Df z#^hZWU~Fs*P@BAD<#wHCtf_4F7pgE@J7Hx>iw|DSwp_d*;-5+@J0?Iaeym=Wj8W>1xB_g5WR zoaa1x^Y9@r5Qmy#VJttqsA5s*Px+OX=5}<}fgdXmBxYI4S^Y^^1U14@`6hyj>%ccw zUt#RhIfl#*XXF3V{Kg3u?sTW4`-A6P5a;qMnQVHEd<$i`EHWD8F$)1o1m0IE%p;}8 zAQug2KzZWkCM9;VvkjLzyBx-D!Wh{>r3VF_{C7`rjUqC!R2?>_IFJc6-__KqOypM` z@-rP4joU$BBUX~($uQLrhy_m-%r`y_7irn4);JK{%Bo9LW~-Y1L>z`5F}hu?27Iaj z{eg{5Ptp)y&~Q~whzHm)=*|9fU@))wY4pJO3LkH48U9ZZ=y**X^1Q@<)pH;u#BQBS z!@%%xmbv&_7DvgJ?zs(9h13+#5D@bvw=W@=BPU2Ua#e};Z|^ZHnI z#Lwxg*w7td2I8?^4qD1FXpnccw=h-c;W%o~7H7BEOlTydqIx=(*eGGIb_IkUY1a%o z*bTfunVpthc)o4g4NM0d(PS3a@NUQ|ZaQrz67|Qpc4hf^uda#MBsQvF#YI>hojp1| zJrV9hOmzMlks`oehr)VwznUE+yuLeCse5~OzBBG~;|>&c_tPBG5`arLvg@DcjlBEk zVxm>Esn!9HRPx5&M4?#Q7&|rM+a=d&*T)BYFb$<-(CD}Tq=hgG(EJtBZS}wfvgtvq zXM^gck^8k45ZkNajrH0UITofI0P>q#@K!JeYNZ<%AKsXT5Q5R?M3a)foh4BOWzSR6 z)T{wBIY2TcI224xS}!L`GihK;F7HHu=IgcBkmfly*K-b>Lbcs=Vfc__VE_D{`d3+{ zCshwHm|$U#-!HH;pK}As9w1@wE*HXF=ze;z-nVU!$Md3*7b-Y(@*hqb!PBPJjROI}FA#ylhM^qef^=>RGno01 zocZf~XY-|tcFKB1hLZAuKl~#9?gbR}A}s+TuE5Z*T)qIoERt({ zZeJA?6pG51?o~KV+kiNRQ{F2eaC{y<%zyyW8jVAL@DPvBGsMePp;Bl4Jl|&I2&l7B zh78-o+*dI$ey;~y93ZVDz7H8>A7|iN@1~^jGM%bG3cebo=!@#mtK8;Ix*qYe_5|ms zcl~(=G4(IvkM=4_JOHv70jt_*=O-jRi*aSat5*7}oS?Cu-VuJ!K64Bimb-clH+>;B``)aF{(FFFJ$NkhmK;hz#YcelyU z1f6;%2q)3}hh}f3T`ndk*bX@Rj||CZf3l!|b-1uekq&%s4M36Zbrrrs&t?O7)k>_f z*_x!KiMnkU-;aT2_Q`>S)M|`|iwoCjV(aN*qW}$V;q>ft&|O8kx5lbj9NJT%q@mfj z@9OGJ69f7aKsOsqumNt1UxgYMb$44cGzTp}T3J2^RPf=Js{ZL(i^lsZo@F8c20_8+ z9^;Pm^K)LJVy)Y0SY>9ddZ-By@?H}0w<9fiEX@9I07r7kC9aaE zCTI_Vp{h4pV53*YCD+&2x5DyuwcDA%MO|wg*jI9VAibyRt3vU%|C5b9}9>)t(`1Kjel89H;kz(+Mnq zpxe+xP?!ZgGT=DI5;@pZ)y3m<(=SOQR?|~biR29mvVb2oDj@9kr%0j@Xr=b}_ijb(c3*PK4_gT3qvB_kTEm zFJNeRI#s0nY}@MQIeGc&lIYpt%3yT~~Hji*hZ_L2L7Vn=a{A;noUz@Twz z;Mq2sEiVj+Aj|KrFWf}{RC#!{i&S+>qXL2r2Ck_kv`a1J{1AyOtWZUZ1!l8e9;c3a zR00wLv`g1#4mcKogs;FHTaa95#Y!9E)BBQ@sX^+JS7b0Gxf1sI1rcvyoqxQqd6+TW zbE)J&eUjH$W)Avxkx5LaNHZ`Nlq~P7 zvIq~>qZ09hX(nJ3?{hlTiF7HPK((o8%PU}AYLnW88cp{Sr>XSNEd3h$~(!n>C!m@6gmaRXS_mltLT5F*s1 zz1rrwDwg~t(DHdHiKqg1GcRumSjl0P1nHgodx+P=N$!Z~I4l2NlVo12PYbmqKn7+DfU=r``a@V4l=uFT%F^e|DJEMiC< zVh>UpZhp@;&hLDkr$xFJ@yiE>0RkngzuE?%xDUmj-InGZ>~me$huQ494zGAPt+(+T zn)c}TneYp?e9%b3hB0u;PM=j99>wj2aYH?<^pROnq0T>kIB~(QFg7EEr!--XdmnuQ z{S!GB7NC|-|BGAWbR$J`*JNt6AtXM#4qh6928AUTBhL-oz>U>(yKt%0;BXwCbqy`z zMgoIUS(tvnVpm7A^)cc+q~eT4R5Zl`cn=Dto8~#2zN{QM-}of{95^|awJ0sj{N3aZ zcZi)>X~t`qtS!s!IzuC3(!?;*XIKr4Vb9$VHkjbG$Ua7@{8aGy>XVFM0O#Z)LmE*p zO&Xrkn8c*xl~Pny@dNYb*yR#N>_|3b`Y$g#?rO6uABx=p4EvF)U`A3_O_Wen~K|lE5BoW#d zhv7o@gn6-%a%gG5VBnpEdeYIn7FJoUvC!v%8QnnZNNfJI=};7pEQ)DRLh|Fhqy(z3 z*vMFmnHu7In|=Qo7zeUe&K6*ujs1eLduv{Mb#&bK=bA>a%Uikiw6hWT0O=P>F!%|$ zr#etQPVe71SOS(b5cC5MRXP9i&t%nXYz<*xkH2!h8Ld3nR~=BT#QVOaAhf!+r@2QR z0J=p%(C;%OKFwQ3vD%Ikg~~*qrfx-48=W6sCvEPrVNwv6JCa^jd{ z5TOV1*k0)MOWzGcc4B@{RFMjERPo7h^_lLY@yxv#d>Lingj@MyKnQ{#j9NZTZQ}I) z^llWSEhD8sIytF9-jKk5Z*%i0fS<{SMgzn~7^O?z{SBDu7C=OAak^_?kQpYzot-K?dd~3*zP=9$UZV)Y+x8RS=AJBc7|fQt?8 z`+j1>`NPnqvzQ*`4IP#Li~JJ zJ5GO7(NLG%S$@W`TY_!mMf~0~UzHTnPhti8UBH=_*Y2IIL=VQ>I5x?R%cz*ERN2mE z;4k8QL&-?iQ@r&96O%NFiHTVj4*uIs^TRiOPQ013v$t7SUgU7e=Wp-*q5aNoI^NIQ ztnUDBwU7y)x$A}h^Y4|&CAs9;8#xjmmkZMh&d4`c^o|sOIt`!%%lR#Dd5Yv4@!3oi zs(&`SAoKKWX{57<&%q7_)}y5|`NM`IV;Z;(POPq8(*)(`9VLAhs(E_K;;ToljS+x% z-yks}?+o0x9DdI)x7Lgd$EuXu6)hJ-6B{uPbmTp}f9b)ZH2`$rTb87r)bcn>+5|5A zYE>PZC}7~QP~(F3poa*W3?6sJS1!P%i7iH{;a+D63=@`lj7-riLH`gM%MG%F>gX!2YSaa-uEKP4%w-v^)JhB0QRzt5}u)M zG5k=`mEUPTEwVhOHa8)t7ihO+8bIZnuWejJ%5ZxY<7w7V(?rpVEJ(tu7)yP0m*er2 z~PY zZfUK}DgE9na9esG!(k)xsLUL(KjjH$Pl2z+e%lw`);ZMYS4#tqiP~RVBh>XjyaOjk z{mm7mb7$-~z|Z=IEL}~VaH*|O+qg>qVaOkyxx5V4aJzgio}VzoiV`52ktCgfT8_hN zWOm<-L+u8H&-k%B&91QSF`EO&ts2>@MQ3IUpmCh(8JcI>fc|hktO3P*gZriJus6pB z=t~VzYNg($U}^znhI4I^v4ef)%H5CG9D0X~OY<0}2C-ImP697u%tc zF*5`cwiU}S)?}b|zULGJ4IBoIdVh2o#2m>R6i<0!-%-lEhmbBo5mRr8Lmy6U}Dr@L?+c?)w>crG-zB zZd@cy8|u5OQX`J_B?h7WO&n!G`LQm*#))(~BgYtbxhFNmJOW@v(geJPiBdN9539PdC zToV>E({L0*7WkaRnmK3r?1yDLI4(6&*3JhJw^Avsd!vQs$Uva4t+DLjj=V0aqf~E& zbRpGTP?e^0@cY)8#NzV<=<>~zsb)co3BCy!{c&`5vih$afoP=8UZ@N;il@XLa7N^O zKR$w@qg&7&^?KbuV54_J<)u^d4WxpkJ|{Hk(f#fH`e*1ldjZcW37@;MrLMmPlG)C@ zNy}D>6-r7INekUzzZ1M3bxNbp`qcj zM^i--@_WKHPSj!qtbSVJ^`+#2cKKCkYPZf7DqzdMFjsNu*_Ct=7M8^kAz8!$N8%1n z><<5XM<7E<)6vnqZ5CiCl!|k}2)H0(H&iYcf(1 z$4mWpfix4aCQx2YS9t5LBL$E}p{v_*_?z1Y+n;7z%s9S%{W{mWu?`U32Gpv;u2Bmh zRfjD%%>9YaRg~f z!P#LR%Q#NV;m3fpvjRN2MF4rRdqlNyX?t8(?HU*hmeo3kT|Vx+pX(=~++boB&uQ>3 z!Mj$}AMh*ViZJEvt_CFZf$ zwhEVY@@v})Ft@;w^0?*Mp2#%^d1#nSv__2!vlV-sv_aR`m7IjEz;#T?0BMowhjW3{ z1a~192(cgXm6b7?Q)%J78aP+Id|q2WfX{ArSXw@tfS(S3_W83|*mpW7!V~gh)i*bF zm%EeqzYl4qO{F#G(cKwmj7(#-`Lp(o6r+g@&bgL|*k50>zxVh|5=@P?03RZXj(aHa zIKbBVS_TV}oRM}ZU>W!70VVdm^EwWKWq)GzjQF5UfUOw<{>bW|pJP|mUN7<44UK_t z7BGf26uMA&w*g7!)T1n?FJzYb^?U2qodF~yBpoEJlK&yc+WMNEm5~L^ zyodyO)jDGq5~%0K>F}-OZK^cut*cbQWfEwClA6GCpJy58ka$7-@!rlQ9C%}}k15$02cn-(CB?uTp1;8x}9BqPY zZ{vnm%=-9$$k5iXFPCE7MGV6U=r^21a2Y!L)fznSqB>-|Pzd>vb6cgEY(9jCA{(}3 zzyN6KDIu zubf|`S5GrO9RtAQ@953nmkm0Tf>czZ2;n`*ie-ysIze{N!V;Pg@I9Os4~F9zAri_Z zVt?K#fUadGi~ch8&)CVqR5D5$g4Bqb{T6Fn>FH;`2bQ*d>ZgHv^v9a`gO}*3;s<#q zvR3<##NEBUEm*X;VO2=KN5RCD>J2<;8<-0J?ic#v$YAF7`>1*PN2a`Rci9fSOC8%p zf34K}2MaBgwpuL?)#}w>S~Vbw59HGU=a*wwY*Ef15HbYx&GfhcN$GrQk(h;Q<1TIq zx^{qB!d<5?r1ohAH1{D;CBbAk>`)Lu7lLw@+IZrt@TWX%j@7wUY)ciFYWPUw1`e>- z6q1Z9oH+J)?9eB%c{GN-*!MZr>a&#cMH`Qhe{y)PXK3H4usH-+&y3w3Ts#BjxtG~v z(y~O5?E1EsoLd*5;cjiY3GT}(=zA`$u@$_%&LgVR+Q8+*4??0;v?sT}5X*&%J)|CQ zgX`D7vogCdpZ`^fVY(>}=3FSKtGlUD#A1M`5 z7C|~#?9Rb$g`4hpYAB~DyWBa7@rPf`IT}CpkWd>qAIjG*Jy;m?2w&ac6vUGcK7-Og zcXz6ztgro}MHp0?_Ypmn#cQyEr&-wXbmUaApJ5oVm;u(Jl@$k!qK@E*)d+_)Zn1g; z+da&D{db>ERaOcg?zcv7`n(_cNbN9?-ubFdOudf3q7}_sKH?h`) znx4*(hy4)&b=&(C5JP^I&iQjCy`Lc2SyTP70V3U<6vM_ul`x;oI}S0T}#OnutrD$wP~u zmgi>b4VHmKrAE~}U(w(9l|`p!(#9F^Q7RJ+oZhnWxvnx$z6l;AMArmFk5khcM{SLZ5)`sU%55Ma2c6l!P4gp>!^FmCm>K&yqM%jvqcj2}%G&ifC>Jw7t-|G?E|X zY2J^4(N{D-Dqau&8}j$AKy7XkVr?%A1lRPPuCmp|4P zCi7QV5CW*lEzXqG^#YgT%rs+j3kU{syT|6rQ2fgkA^p~jil~)lWoNX# zV{f*(cW-}w9Dd3WJ2_h=3DqLGA+=`KylgnWw^$0rwd#h3DboPNelG(1E0|x26}F zqb@3X*eaNzJP@-2bb%Qxc z_aGe=jD8+KO=31i528S8%>D@hooH#*5Rf=uE{{8MazjMacT9#<9!zr%fmQ0((bN)T z!&E-hgIu-eZcFPXw?VZ)GO4?|2RZK%%^h$>>8t`sfDexBZ!?SXZ0-hqf!^C(^SvP? zk8W=}Vist=$Hqoo9855ddGkOlF+v3s`~G+xRReFXv9V&~VIia`eBW*ZC-oT~BmK~6 z&)Lp=l|Wx-oaOw-!Q=lyn``52_y;yur1v8ROGe=x63r>K{BHF?cXziu6>yA7BU50Z zf*2&V(1!HV(ra;J{kpIEBp{@P&l8CZ*BbMfm2}Hih7l&^tOPl$4x&qFz?DqNler z;1M9*WOvu8o^tWQeSnY~q|D~((kLp%)d6iSuiH)3wZiI_K{YuI z)lU-d|A`dtC8nuk#okGSEq_X`&{wBYxFhfC^hM`VOkDij#&oV_gjj-o$^B@@+|}Jp z)2a)}d}3vdqzoJYt<$p@+PuFkTWTxK2VCsTPtSZ*tnXhC8IshfNl0BQ&zC`sbWdqm z`y_1#rl6%eaqGj|!9e+u{`S<=BaKAA+xdS1*&z&kkzmR&5&X9x1-YGpyH4Va2&{~~QR859)M zdUHBhZ>18r5lV8^RI`7I>)r*L3y+H4%bjf#z)MwQG}as5EDld?%#%;wiPbNkF;1GY zSa3bwpko_!hVZO9Cx`Nbvz>?ftmi$x%~_HmtL+#-;gQWDo!F?ENE7`VKi0FmMS!|LIAS`s!u9TR{i!O0e}bvut4Lvir*Y?9W=Jj z+%cI3{bdP$OsMlZmI2PJ4sjxlV%q8^48B2o+BSAp4Dj6g<*6}S2HCSRZ=qT<1vu#c z%I0?6W4y|xS38Z3`r-3c=*=2>>e`5B|F0CP+}z;^3M>@B*V{2N^tIYT`oRf1^qA|b zwVwGc=|hj&Cc(!8Ugv7Uau&)dRmcUCg5KvmbEUc^n1^$#$I)v-)%I0GnPxJCb`1Ryk!FHyG|d z_c-9NTN%u4+&WBYN}>}S2|Zvo!Qbut{(n-h21_5QSKTwGu?*Q$`bRZrt*OVhcSQm? z7v_Y#zcwp6Os6x{l6UJI@Q$`k5vp$O_ZdBmNV-(mrYsf%F+^2V;3*dxE)jM!jUgq( zb+cBl1Ui;CLH`GI%sP)6Kg^T2D38y(BhdTZ)w*ps)s)ZkQv4R_J|c<&iWrl1l?RaC zJACC+qEP@cx%r$peQ)hOLFsvf4-DjO&h|q zY~e1umg)Ecwtq!G=97-V!F^|PZp{PKuT%c;q*^<>T`|ad!3^~v=+4V)xqXb4Bq6}V zny9LNu8M3G`TOPcGvt0;aqQ@ctV=76ZF7kI?l=SlgO;-@{g2ShdPi{b?=zGznl5GV z>jK^dP>Tkjp%TDtzzz-_bsrx2r!?=-aol*Nw_Ip0$*;v>&MwzB5&-{igOU10W7h9H zg<^Gvs-E|7te;-t#9U%`3;n$?RoHaZl>S_@x$~f7T+cY2ph!Kbkau7xFkYCe#$~oW z1~Sbf48CbyRRu``)*R6(?=Sa-buCb2A*d=YHajljMw( zn+#GKotaOemHc}Yj0q(NH$lSiKpF%Bg<4{~qSDB4IBA*CueFg7p_?UMjDPJ6APY0- z$w4;>weq{A6rFn_YFzx5_|vuZL=fF*;Rf6 z(?fpe7g7vy;{ZXfe2xsffc{U|h8bwi>P%FW0|G?KW)WM=x9q1Vr>6Kw{c4KJ6M~pv zTR>1wjRwQ`$l9xCdnR)0uq9eh7#aBuat=V)RIp^^LF<=2bKE!2vyFN~O|h|uRSf-l zMNA|sJAfWm2@W(UQUOF84p>Ukz!7=0S~>@)Z@?DSZcnlC&js}Y)qVm<2o32~yAW}w zDQe(lwK|mmv9#4<>BFHb+AjBOIlt+gobTGS^A;wKEr@JnG*Obveaq3H08RS#>t9!k zoRc0VEysL|uGJY-zo_TCg_OO9)m7;&TRu2A`4L>ci=mRS!th z@%h%r-_<~emgQ`%CiV5J$oVVCmwOnQ|Hv<>E*hlUti}s88AIptv(v#I(|3-n*>GL! zL6z@^b#l#^^{-65*Z+t-{#;*O6I84F^Us!pv_m@l$yJ!KF(z0Mc%)8-m$tGg(-+8Y z47gYSCJ$)}yb(meby|K5{p74bZG>|3NlB{Jbq+qm%>D&&hrZnszQsqUC?=)cDD|mD zr{oJ%CWnyQ+Un!oZ}kEuVqeaK9sPRT`xg#`w0+r@+DJAt!DB%1@#5+wEr&GFc}XiM z#QNvHd`c@Rc_NVXxLSPsEC%8Lehu;SMx+re7pwi?D3s|BW-9I<>a0(l@w#!p=uN#1 z0()*yub7w*s<3(}``X-e2_dYNM`4{2<_IJm6VO1Lu5`Y4i7qhnZV7g5HG2dr_Q^S*t2 zMhD^ILG$_wb7CTb?2KPe`a0UPXHKIj2Q+g@A8el*Di!~1OE1WIBJlR_1m=$>$j?{; zI#H$v&85JPB4YU=@bd^jl%&V47j$y6)zU$^f$NW(+PzDhoA>U0GpgO56(pp;fBY;p zVff)r1lrTnos}KTP0hs3D%7#{lThpp-|LTpZZ+MwDfm)2Jh$$Po3HupJhZjBhre@j zUs~~g9QmD{m=LR=h4g=JQMhPotDFD18`7P)K>ZubPm? za#=8k0ZaciiRN8Q5~l7gPQniaLH0Jy+8j;GP(mgs+Cz8WCL>QKa=~I&URqLoOnR%I zkg)n$U2;ji|Q4SUR$+;AgrE|!7TIgfMQ4!0lRDN(v@4-Qn6;AQ4Tmi zl%3Nf5JyR+GRKLj|Oq`VB4uR^dzfhW@t9{8x+jhX=U+}kz&~ z*_%^W^*w5@=FdQqXbHpr-&S3X$cJo-`{ym+XD$2BfKQ|CJyZ9BIf!dU@ic?PMGPZx z#s%iKT&7*YK|3YUlL){a&gXH@<r z=J{z<>>t!nvz1-cA3flNeWaKfb=<2Q-HE#QEcb{8PJUV!8S7#HmNbA}q?E_8$I53$ zNqNma63d(Y>0w^trqVTbqr*3L5TMtQ7_DRW@F^PIdrSC=21lN@@I5zG6Iw%2;khs6 z%qc!_3Yh09``umX6=}A(KIcw+&y2Hq$;9}hGuvViM54x_rmv6 zXItJK?&;zr-qLK<-I+f2wa|-gWJ^2IYa9KU6(3xOvi$44O8$Der^qZ;a&~6q?I$56 z$h1dArG?s5R~Qy1n2If~GA5JV9~RS#Z$VdC^9GKwBM_)LIC&QukZ&J-g|NqgzvX{D zz?L2(ZrvYndt3mAN4Up=dA{Lg59l$2L9Mi+tiESj;CQY*M$YNP!r@H8c`0GZf`euH0*_U$lSsB4rgN~*%?~C5qsi!kBv`6FqamIj02)oxYL^J zg=DfjKVA!V`~gn)UjN5aG{?56{K$UD&q7_?g*{k|DW@Hd z1(@_ed0=X9bbR`T1}wNIFUaZejiRwaY~+kK*&52+w@JG1zex2(#Kd4RQx@z*N5}LW zs&!1<+mgfe)akDC;3oPBV=o{eH{_^QdyzIz z6037T)k>YK{oa;a+{--@JI_hyN5=(o@-v*2L1>qa7}KC?eU>~cGlrc~b_njTbY%U!`0X44sXwQi89v~OOZ!eXPr?P+3TvopJY3nsx+Fwe*L5ZhI2C)o>))+X~r zyA+CmMbSW!_$J1Odau+!sM+c60*s)6cI>GxQr)c)=XD{voKjfwrQ7nQ1JF#X&$0kr z3cU7Mz3d!nJ({)c$m`6cJ@cK3D*ZSM^yFyAkEy@nX4x(M@_J}Sf=lm^DIO!#0gQ4o zPvczSor+F0L_PtA|&a=j{N17^Q!C@Lzx?vd62{;j2FqrJjm^RO> z2C^3(BD5mTwVrIRnkrQLi6}n)jvxFo0P-Of5Gqo6!{Ww4zfmt!U(oQ2;I>fLAFipw zO08a_iU?p#GvWt5oYjy|W5>wQCz{szor8N)xO`zL=VmJ_CBUf!FM09q!`piI_4$%- zhv2ASxVmQI0S95t;gIu=c9#%T;?hk5K)8#()*wMhUosp3Rskw~E z53l<9?_L11ap5=k8J6(-h9}r7;+6Qis{|p1=zsg-rJqKh$_B9!)L`4yeuBd_)tZ91Ih3wv7y35pB zcsEczh?!#kxm-`=OV9cwG^ZkeM~FV_cyw*D zea?H8z;Ux4{=8{*Gb@ZRCp&*YMQNjG78j3bAzgrG(jBz}SXC2^gD0&tCkG~=NDZbu zmh%!~q5x;jMYvjXbe&&qg}l9)SrCRD#btY42Rt!fN`WtErpUM*d8(mVQp|MBFK zwGt}*Rl3I7W(s6=lB(f((*PACIStLT`ha1!=;~|4MpL){f;o_OXmeblqxV9|fw|LM zg(uy;IkGl{$zL5ml=Pcs#(yU(>^}WVR=~O(%kDsM_7aCZ{{B5JI}?CZ1d)1bXA6J+ zBHG-!=^}so=eybiC&NlU=6l3o=pZJRTI9EIG@ROBK~`qJS8(1j2BG*xeAAwA-c_-a zOw$#duzz3WslnwhDhjSOYZ+%tZ#AfxsUC1{2YyseHV&RDOMNH=LlS%B@&?T_e>8=h z8-2m5orY@$9|as47a$N`VHpI=5K#2y<>ny3uLKFAZ4b>s1s7T0wKV5=c!yMskA`xe zybvY*I}v;7NeuFyK$Pt(U^TgMFg7$a2X}0VTWuKyRtVU;38}Nz$HZUM!goF})UHSb z^FPu91Vz1J!9Yk*^bb&mXDz@_;2Z(FAO zFHztddT;gc5jc9ShQCG{N=eWDXT0+*Y1lC0(PHw?UiJ0=Gy1^4pYt77#1ncpz%zBV zH&Im(3I01~`}<;I#BaUIM`+WFvhxdba!3u_IF*(EOihfged>R&w^#jdM$1M?m@SaZ8jI5~;g%kcop0Vi=Ck0j z^(W_^*JP@I7@xS-WLE=B4W@M+i#4=<6>}Rw&9Y}%=h)11(v>#==o2o#x|95^)poUw zy|G%c1t1jyHTb;yR{d=^-k!XoJZF=83`m0$#O1D;0$BOyA&J-btA==Z+W?EiVw0Et zJ8K63(Ic60*l80|5)vj0Znc1SAz796qZ{xIOodrA-?cb`Wm*^h!epcdQd<7lkvEVs zR3B&%IPK)nwE4P|EFxuv^;rQrkN)@bKaTu6J_A*E=X#&H3X?3*1JKZj|Be~~(TpGt ziCR%XQCazwE7tmYX<>18xyi-b@?NZ$Fw1owt>X^vA|TGJO$soV1-FLNtW)Xtr(pq7 zErXPS-=vf5!VtvZ7=#i8gl4Q^CYoShl~u2yN zhG;L^i2`2yT&jAnRa0Ld7yH%G*`x6T2epVdjQ?%J)D&IZnoSRX?VCktlv=-9d_qF6 z>As!f3vO=CUWBk07dvz7eKGFGYa?uIliJJ6Osxg+3#M|^b7|uIXvNvz<5i!4|2~Y0 z2~Rx<-&+(IJ)99&>}54mZBRIkX||;Q$!=I+-HT1*c7IaYtLXsET9lG%bbsr~HX*OW zYM-yT!{#K{A-q3j9#o56tq^k(C7$`laPo`5tzAsmt7wCmoP~yufy7b`4SiS)WULw_ zn*7f7V>m_K!|%s7#4aZi9mOyQi?bHJb+a&YkM)zQ)R_&x6gT!=DF@e~;gb_Z8R$dr z(=IerKIm*+UIw%*=+J1rIfv?xOroCXSmpW8_wsv zbJv%K^;I;M4{xj^M7r$rM_b$ScFMt`nGy*KnpciDZR?}WYTw(A2g4H@K|*@7c9%u` z-~jg}`~H=pXvJPU^XJehenzXkv+I+s>$90|(Q>SX!t#Q`LJjWf$M@&;Hck!3s`Jcv zB9)ZOVWVM|^4+_ul?j4U;d+PNu;SF_S@-K33G$5$ebc*FVaXo%Ha$ri5We?srvJbW zg0-+`wYE>O^8Ju{(Dq<5Z@!%A>Y&ttnew@zFd@I|N6`wmTAem%Vg02()i|Fc3D3## zDTtfui{{-m-Tcq@Q&suBr&LBp=HtiLDpC)>rXBsFUZF@)iYbbO_KomujLt_y#9^Sl zK#UPFr4hf2GAp+2&>`7z+XJ!g~iriY$h2BvN>W_S8U zq>gc9Lv74ko8ujOA;(m@LZIF{IB^02Y=xim4|fW=Pm~&@`3l259Ca4yb-4<|LqqLn zt^IN^Bw)PB zCb53Jmr1iT*7A$g+0MpFI#r|AL!9_@y}t-@+dJBpChT8!gMl(GpSN#rzRK%+vr?HD zwK+URX<*%yPFsm z-COJT>N|2`U#A_fuCkI{ccuy&JeJ4rM5NYxQiSqa(Rg{X>6D7XjxId|sblFOwdP}K za3>+Dm0PKIfl?{;N);ar0~J z>A@XRW)Rk3oGOfMb0o%#WRAke_wa_r?5dPoQDSk|E<5~-5Jf> z+1}JB#(l$h^ppNBsi%+c%gwB33)ATFrza2+7{!SPV!j$ygoSGPny8&fK?^te)nTjO?1#)`9a4n#0x++0C_oeZI*k znI{@-?n$n7;6jw7RxJCxIy*r|Ms`Wa!)4lo-|Y1SPo?rxF4$Mq>YcN9TV#rfl;tb5 zAQbj_aSX~VjEq-DJ>|@C1#TOwIG66X4%^bz!X4*{4ujX{?C0t)h`Ae$21wk&IlNBk zHQG@27&h(0Wy>pb=$Y}?YKyPgpK@0uUtB~b#TCw3-Jh_+J%0Fz2J-YLNnDxvdVQ92 ztodqHdY_GcR>tUqkt$Pdwk$?*VPz%C`SH0_e>WKbRR&XpmX(%XprP2=TKB|q3M@lw zxi4;HQaW8U4Jk$9f28|1UeIrkW+kxMNFVGe>jZFd_2aW=94j^!&DYRL5$*2-B1|N! zEd2NR*S~+Cn<^AZ*C};D_%)h5#Q*-D-|F@pP$+dEt(#7`e^}m;;JPFs#>b|(t^&WG zMLRua=5l3zw3st)+vDD#P;NQP<8yD_?34BW$Wb7`Eq-qdfiiQKee*L;rald{91O=$@7(WjNSD zUcY`li$FQl0W0Uqt-5Z%=6`(QX!kAN%&sDhe@48{&rDxC_wSQLmFnSmp;Ugc{dX@Q z7^}b9c$2uY-s!j7iIarmWiC7Z^=Ku*2`(n#Xx%NEwPs8&tN)1th>Rq_Cblrves8ol zUXMmh_4ssNQX*yb@HL-u)50#^v+Ft7+Ed9_c-etN{jP*alhb;Dob_0yq?cC$iP;v zZLB20T&4OcwZ3=?k7MblT%7}BS$-$zTNc_c|GpUMpGYM)H&|36P0;)Z`}U2;WoKS0 z+TaEbwxOe&=3!pP#DIcavnOX{WMq>;bFt=hQT_G>)#1ioWxIwn)@~KK+LhCWk{JT|3Iv0 z$y*kk?u$;ezf1D#EftNpxS_~%Py{|cM^qyUC(sFPa*m=ZHc3_|p?epq|`QjfM?0u$(749*|#m|NZN? z=}#IQR>Lk?nEw7T2?i|e|L@k^|L4ng6zvm{0RNu}2{hoPJ}|wh&n+v@d&1}~Y0{)5*>>Nw{d#_HPBYnVk9t_6F;!3Z_8I92A|$7wAXp=# z+1qqAaY#aIU}6|AXJ~XF*N%B^Mx8T9$-njX>_Zh>p4z9iJ)vZ4H+i-I6T6t)Q zz07uflmn9YQEukuAY&8kqQ$unI!N6{Se)V`zJ~d^TsdiKe!1PAd>qw3-p4~Ew!f5s z6BqI3J1HFR!Nwf@uphr1&$w&1n!rt`c;w|0db?ykMsl+9%+Ziy7yipj$Vc4yZZv_Fh@NAI z9y3RLZptQMZw_jMBKWNY0Ohe0$*OxzzGfyS z<)`z`hg+x|=DhbW&nuLbm1|K;?p6-=_HM^#H&al%f31=phFQ$F_iJTv*L*itnV)L^ zv>sM8tCJLKw~ps@9e|LQzi>mgOxKv~7n_BU!KyYp#+OjznIogTr1D=z;yIi_Dl#dK zdAx?qc2cgTk??*4>VtpsndeOXoES5uhN{-}a%!0(9@lZAWkz+M1^SMQ&UJi)hK=i) zSXmW!$w^7M&0#HzoqqU#KbXuM37VI>4iFO z4T^MF@_wRrPA$H8Hi%Rw^4s;+?#DIYFOKDK4&-B5xW0SWPp?*rl0jo6nlbk5q~l2A zU-{`p7-I#7VTq!rr>Gk;E$(yP>g44hp++d#0JB=A=&AN%+GG$~(D~121^PVi#fsgG z%L^<2jSAh(wL=8y1jv*BStuQwY~+~Bapjylm!m2>lTi|7?L|dJnt@;|QSfBLj?B~+ zxy|aAGQGHN=i+g^De*@eZB}MIK?Xi$F|^+L#>(Etj(UN9Km&fq?K?1@^|NA>Y)Zf{ z+|eCX8nqctTH^z0sa{m5%vs9{Yx&H1>=xAiM79zjASq+B?SvR zXivgTy&dY|(FN*!MY(-ek~p=5a6)r3YUD|O&adTfhn26#!I+=3Rv*(hzs!0nSuJ8p zN%F4b3)}6LUNq)u#eDW|+&C{!*M1Z<>9?OPb=PNUgL4`TCX?3NVg`d~H>&WVesF(O z&f(2H8xjm!Z5J1pmYW4$vvY(;p^syK-reg)o;TU=M|_xWz=Cagw0t}{MW^T%MuKrs z`F96Au~kp=V|(W9S{D0+LgAYm5@RjDqQ_{5Xfutyi6*#RNhi{>fd(nB?_eEf5qw<9}1l zq_sJkosCb?BTxI1IgXQY6Lrva`=BPS6|AeSwOwkFyd;!yHXHRpUayN`N0tHfhB~+H zdTmw-YJ1ZRZHTaEF$`2m;%Ohznq^{_GC7Z4|f9E->{#e#c7}}TUlSd_p9X%)47Wg zc0^28{q!`?q{I_T;NEJ_Ig+2x#Ip`^(FVBK#bl$>4sj3ufl-AQV~?NHn7N>_2PezRUpN+0D&T*0*m_ zgaOAT+wRLrER^o3RL1v68I7JUUxg~EOMO?`C&~373+3&_i+(h>99n#vs0M9}WXLo@ zQ}_nwCvC?MQ9Lf!QEmJNZV8e+9~9a@N&Z+mx!NA;W&`_>OI|h3=)t3u!Kaf$(v3R~t{lJKhzShLB`SU8mDhe$4)j>5hoaCCg=RW9o9U@%aE9mHY2$^u06$%D_H&I7B% zbLWCv<9q+Y;=rM-=2gbk=f6JGWnV2t6M^9VOeb6Zhp12;2D>>LX&^6O=rUx`ruq*S zm!3}L>vJ@V%!8N~&p%k4i+;B1-i$3l_y2;Nq{FMskpq7^fr)coGoVK%C@s(5l7cFjPv(q~F3P`Bm(eOt%0>S3n( zi3w-WBTSEY?~qFFVVW+LcDfvcizM@hH3jB1l2jw+`FKX}@*we6IJo41xn@Cj2cusK z6o+rTbf#f2-N78_+i%ly?FK}Th;v=9FuTIu=6IXTXZ`s@kY&Q{?J7{uP5txT zi+@Z0_`!X%zh^w6HrXp-2{=j%9DVFECxa9YQH0drT5O-mbxX@uu4kKWRq=&W=p4SW z-u=5w?dA6(hu?#dEum()Vasnvs)$-lO-T7m|Kw;!a0#Y%Jkn@iPD~B2vC9p6>o9EY zpO`(I4b&~E!bz=rL=+?>2q${(I&S`wRbspDWJQpSa_ONTj87T3uT_C6RG;?7UZv`D zmE3JCnr4pm78cYYs}u3w2pfrrXd-x*?{hSu;yDyEW<;sC-{yGQ6q81bEBL|V!-A2t z%gMXlK9e&7iCMX!`ryAk3H-~5V|h%j3rUQNV^}-7cP{ZQJ#u-FK0686ymi7!R=<*K zB?(3anJJIAU{*K8%KzNm+yFKe>AE?WT$Ut1=KY93y3y`9MUf3N_MGtPh6 z$12_u4luSC7(8pHZZ;#Mc0LI155N|vaciBoZF{zCVLxE*|9GrAc&%Y9LzWsptc{5D z*oYpjDuxpY+pWOk8MYxB{-FQQ=qvg!Ezo~rH#tc;qRtx{V{_uHZq8Hw+~KDuIDIts=L*z*38SZ%Y3_To^JM`Atln_;9l@?Eo=_M#x}*q7{Uto zQsXBMn&r|aMGxVib+9_JfVH_J-%1hBgh%^Cs1aQlJm8%mG|n&_Y83dK5&7LS4rQb} znksk6mT&F*TYt zIAO-+d|Bj%nGOP31WZ?)?bWkX?Pj|~mif~`8o%X8`-vHDFT|^0XR?F1ZnGRC7#q`p zG)jx^s1{nij?&Cdk15ARbd+YeP<_q8>O)Q(w|lBgoG)!1ka7#^NBCX zALLSnp_lQyf+af7_Sd!?PbM4f{pGGO|FV2VD|U2kGGxj^uY~m7+va~i7IIq^U|%A! zeJ0=q3s;fgIi4;tW?|*=>O5O$4ZNWuFxf)X%alOT0B&|E(olFXty=B}H9Y0KX*DOM zHb?I8YcesB4MCWU1rMt4r}NeS(LX1rO^woV$RsUGqB^ag%E+&|uY)0@uzu~N^a47{ z#7B2#?uR*3hxl+Fv_%XsMEUb@C&`AN1Ny$N zTe-DfoTaLc-E}<=MPa0!x{-cfY1ZbI z=OSsYw75fkgRw_TGa5{T6w@I5n6+9=_VJ$7%B<~)cau=%?2|x03~i}Z5gbawJP95& z0Gz}*59Fffg=AVEe7|#~_3_@RdKHo2<4+MKQ-d*~5NXj(D&%(hLa_u-m>y1ZqQPgV zyl84TbPLRQfEbwZy%W_>_`%GJvO%)FA^^Xk?Jl_KJPDn-{sR`v@kpyjka3fnGomHW zSj{(1P4Bz$7JnocM(VB+168@BEj9PglZaH^E}aim&x4_+k83LXz*jVS`lN<064Y1z zEkL;?-7>GKJhURD)-R2oCW*eL$Hchk6W_l@jPUQha+q&ph7K@~$T^Ju;A|7aHfYTl zeTMO>m-l2{Zf=Y&LowaoSnVd-WGDlxWuB0#bamEt@6>B2^1*vj>Oyx|jjKVdf@D~1SrUxW6vP^j7G$NxeSN%m zhpXW)>|GB5%9{)%ePrvk{G5vrw(SNVMxa)I2( z<7_({ZV!S*KKrS$cD&vub9K%KOJEGHfccp92B%VC=b}DD(@V_15I~LMJggo43t}3K ztVmBILEkv55FM^>)gkKDyGFt8qiHzL-x;3Qgm_hPnASX00yGG2(#fQ%&R1mtid741zH(R5Z_ry4QPuJ(kqWE<$_RLDe zZiGu3f*0||+Xi;*j^1fBZrQj_E$QDOUJq{XJ~9CNt=qcstgU|HX}!mr{$@nqP7^j* zGIyo?lopZo$8czM)b(>bTtHB<{@2^Y@q)PJEI0 zI43%b;T6H^JKd4fxxoSolvZ@jWGp#xJ3ym<>NjNWua%2^=P{Q@K+25rmCxLuC0Rx{ zr?R#8w(U;?)00cO{Xsh?6yA)tWCG5&RccFPM)>h?rvUlw`+6De!gk z<6XZQ?9t!0hf;5At;#pCrIihb;)$KUX>)(zEHBZYIR}9$=01MfEqdlQjLv&Re`RNUkz2yy|3k|HcD5dtUzvg3_0d5?L@n-p zLN$G}0t91Bd%DXcIh^^ksOLX4MDe{~_`>M|AvbSYtjFv5(1!AkH9}sUZj-cDIXSbo zhar7Ne$k2kL=+URYz)?+54XuMqNp5kPOjn^3SQ#gZy@8yzKMTUvXun6Cor=w`-Npp z{X)!=;55ipGnYk#^ecxxx6q2+CXu=Id(wSN8mlakMG`_Qr>Q0D!B@SFg7b7Q{tA4- zFrV?=Zj;i~+KyoVEwxp7b0r<$&$Ymysq*6prSnD^>6+Om^-0iA2Z1t}a;&6GA$7sQ zddiVoU$-nr4-1ukya~Fi`~v8}9dY>H4!yvnsBr(Kh{`ra^J&EeHxl0b)(rMCJ~ivT zdmo1AQ$Q)lS-Ph>I!|ngd`i&tJ<@L;5$Vsg71%a>Cy?sBnDFMW$1T%&zY3MI#+-(x z`)@JXOqpT{Rd8YpMwHK^j}nqhOlt3}dG^$n@#Oh0jGw!6{7j7=4oo%rqte}Fv?Lz@ z%>%G7p+)sSRqy{qd+@)+`~Gh_jG0)txX$bMcmx|>A8yywR;w>Y3c{W&E!i&nUv@ww zpG(=k$N2crALoY*cFtDL4lT<-s0TgYgB1NB?lvKNWp*8ld`<2uy)NIxJ znUQ@M?wl*KNvd0MRTi(^cQ*l6;4)jyZ!<-GI`IgzKddw*p=?e8(c*f%(W2LWae1jQ z%C$3jQ>}+OcGjIzw0;b;Uara&qn z)Mb0oxs!I+-F`{n-Ovkf?;wH$OTY}Xa$%H9f($-v1%vs<}dUA9lRqD%;>#*BB%T+*juER2utk{GH7 zJN(uz$hCeJq8|61`Mh`Q1J=qULHIPf&DJs_qMl7|=mTV33E}hH&9AiBn{2wvgBxYC zXg&r825D;N-`{8faOHJQ?Rc?_q7Ql)5Exi|vq~-Go+0Xayh(dB@kmFf0e*$9greJ) zzkS>E2aFXc+_+rSvkoI{O(EhPIED<>pv9(^{$jBN0-YsVxY(rK`G&hW!kH#0w~vpH zKW)tOZY%PSNmdykw9aL%a%nItW%^&8rHlHNspu4z4kss zwOJk!qW2e$Nozi>ZU4Br@#|d_mj@cCA zrcrfut_34Y)8z&Z6M+&_8No|Eab!!Z;80B-e*W-~kQj0F%@w?gKInLDc&65KaEv}s zA+wSUqDP6k@Etv9Ct=_{vKP4T`>9XjC8G6cZbu8L@w_s;yLlFHK{#Jv3hUL>*Y{`8 z&x~~jNlAU4xv58vcjLmIuzXiI+MhgHRmchbvc9=nvd^!m=1D!nJ#{1mbpN@|H9_00LAnUUieG?NJZdw!h&^C^MfS~?B0umB(LMjGoYqg>) zvyqt7-+ww02xp7Y4X)RR`{wuhV|^a}{{5SnQ~w$^#ip-TbO~v5c~gAO97*f$=;AUO zB}vJU`iaU*#N{oteov_OcHX4R=RMaizEXXR-QT$s-4bZ|_U+rV1)bRX{q6@ixKsJ1 zPiDWm*94#AN=wVhi1}PRQOgkh-%nN{2D42J?HV^7t2#|nb%=ma94;L%Hl;%-zSj4OpT@D=w2gj1V zK$K{*fo|VX1x1}Tm)`RV@bFlx;VLN!eR)bfHb!Wiy;ANXRBfI$a~xh@W$1}OHF=n% zxNJLZ4m%(BxOUd)5VjPTX^xI!2`~1m{ zJsP3{027)!7aH*_&Ku?&24;OpR)~wtb&N(t#`;L?=FZe$B(2y3yzrtad0$;gp0iE3 z&1jmqUW;_m;tjIGsKy%IwmGH4QYYr)2-*Kd+it!uT+Xs^u-H_Gn8{NdSFi+Q(bG+$ z6|a?eE|2oQBD7g);1+C%8yOi9tbnM+3fTR5*WF+bGxZqoYeVhC`TF<}Gf+Cz&VmkP zhPda-p!x9Bpqufom@oewj4<*$FZ4pS->E*~(^6g=8yQ*r@#D|Ys@%>uUJKOVb7yqCtNMg#t{1DULUE;t&?%ys95P%MQ~u_jAi z1{3rl&3wu0qUr?EOz~6L75v~kSF)J17wQ_*da9g~fWYkSKy*_qfwD`YYEn{?B{Q!4 zSkGFwIXyZuBt$@zQodZ7M$G3KRIl26%1cI8HYG{4UAIS?JNWC@uu}>p+`+GXj{8<% z;FsFmyu7KR&hBnwwS-Qq$SIG4ut<)K;U0`j!F5UA1 zzc5~|e86+8W(t~gSZdO%_4vT1RHD5P(<)Whf)?gRv>$dzU;hUSaJMbdD#)1bogM*@ zl72+h-0W<>anYaPUJ*q1@uu^}!1~r!61T<%EtJBz=PGu2Ln$si-&f2{$oq!jgY3=6#9BfQ% ziC^*C4~DSBpOSH=4JNZSl+ND`z)^B(gu=@O`lfrP#a{J{n++BFM6^EWz^dnF95KI#(IG-t#o!Jt zgUl3K)%2*OLK9+U_S8R%v&O>OA3?^>2l^Iu1~ z2%3a~bv7+^Xm_q&&-Ac<-8A5)jk?-ULT5{5Qr$L3(}d1egoh>yBFNYc*EWMSuCK3A z7k1%Z!QVg4b}+BaZ}ZCFkj2OE?rt{w?OWem>4zgdTYO~F#C*MW^YQdYyByvlx~t#K z61x66wb60j>@SgUGOYg7p9vcteZuGrc8tPX9;CEfyHCoYKz`S(Ee3{%9FwZORebKU zartGdO#w;Ez=H7j;A$3Pgq?lp^qgi<(BApfv$D2&`~aR_yRz_v2&VXdNVt0XIb55r zo|+E?#XOb!bA>(4iXmQV+3KHxoaZ%Di}`kvUx)#v5I?lPvwePihMmRSpOY~?EguY_)ljd=l{Y(d-QRs0@z{_X;6>@<|0Y8KSQ$u4q(ff z)||76vcl%)r9jI1k!DsBhaThJ#qtnIi(1xLety3BKdq#sl;i-}H^FxfvyNJi{VwHF zU?Re|E%RUR;mgF}yAFSVzqciO1f~8KX=c5Bp~_wLh;Lr{U+pB&u`+Uu(|a4&*cuxO znpSU5sL~AY=9Ecgf0upGA=km*K`T`mY*t&8Y{RcBPvQ55--gNk zmhI5N{1xN6Uul0Mg-gc`^S+~-Q>jLs)J=+(t({$*dlcX&!aOw;NjWNAVBhaHIzN6~ z3DODVea2Y?mW3#DJ-%zLZ zD99T&NtrN|#N)IK>{*pbh}Nms24io=Vwqq0WuTVEh!{hOl`U)AjD$;*MXZK8jQLp; zxS!WCxmx#83?=hxl-l3kPIzSFXVcNK#l_iv$=!^bc9U-0bxILmR3LS8-e|6n(!xX@ z{@*wu=XHRr1(;GHRmG4Tr=7TRBdo9&9@m=E(zyuyh|DI(viXHiVr>F~{Bue z>iLq;O$~U(6@DoHkwEyaK)HsPU2$Bh+kT_ZjW&E_zinG^Z{#Lu(BfK6-DNK09^*?G z3|5*Ub`;$kTYgWjU&+a0bi7Dr6~Pz^Si+4THZveIcF&(5gF?>V%a5KOuz2@`JbcEA zmuN-LqEJwF)Oyz}t_7#z!E8}oUf%k_O%U_25d|S(aoa$Lfqef-NrK8iJiFKVk4}Jp zmRcls5vt;r*-w?8a zcHsPBl}XtEx1vAUg;n_t4+u6|)+QwrUNdPqng;5*>rnVXnYxt_OGGll@^ z9iJVmpr>4L+~Bl*l=Mus2{m~cux)Ov2IkaGtuD=tO+%^Av(59Wn;%j|@2p3*3cK6S zO?J?Q(T+FhwWi!sE^D0~$_CbZ@D9q()K)k2^L4W%#oB~Cu7~{PXzbccC9)mdU86oV zL`fbqhnE_buPt`=($VKI=qo**hBp6-s-#9ABw}zaps(Fcm;Su}FlPbyzr}HEc@b#0 z{TzvrZnhcpqPs2t9GpK_SZYANCetXKJpwWh9;N9iAqe$n4nF}Q7}zg&59 z2gN5XBLhyQ%zBwYRWJh4uQ4%@$_c0VHL*Ke)06IniG}TMJ?rr{vV!A8wbZq?A+zo6 z!GMH7ukW7@nkCVbL))>s%s68^+fw(^lnOi;3XZzhtL6&-`BkAN*I>o+mPE=_s_j){G4i{3 z4nvP`JK;5~(5;TK{l;^>$L^lPQ~z9=#g#d~O_w7-5E$x;wRP+5{kwEd_wRoGi~pDun#qoxLo<13ph zdj)qu@qtffAB8Ll&ca~d3slM?9?{SoZJt3_+J5l|I4kNTb~3A$eBgWWVr^^N8A%_O zEIj&S(f(+Xo8V0}(I)2`Z_>V4jh_W|@mjY^2KSYD8HK=E9 z4xOx3cPm`9A`(u&eI_kgQUKBQ`+q70G5=B?f7@YDox5-@0uB)KQ6VvW$^ z;<#z6_g%Bn^|*kAix{R+FSYtU_+4#N(RDRMKCa{*9%9DWYky-=@#zSU{gy`g(Onwz zV&bix8H<%2U{e?*jFOLE&4Igmg}m&gVD?T3LwKB{I%|tGRB2G1AID;;_V!?YdJ=Ne zL+7;JhIZgERtjQbD)$vV-k$5q{n=2RI&rdc!wMj?IMe*xKyB`MIb~oy}_r9 z*p|`J%S*!huy3^>nD?ajr2w~5>bPfpI2V5l6 zC}Q)vv3bL^_xY=rlN|F?9@TXM2-7 z8|mJI(8CTkyQMtyGD4n!sw7we(ts@9`Q zIu2{2>7A?n6M%m^n5oGHxbgl{E9iY%O3Ga>*2KYdvB9c|O%Ou_Os3xf=I_4I{6xNL z?10LCxo%UEsBxp`+#whe5R?eF{+(AwF-1PxUeFR6^V|yXV?KuD8FQMe*F~D7y6>|k zG3%rkufLM&blVrR^f2u#voQ*EZs%a>07B)fV_Dy3 zx5~Pzm?Uct1+ogHk-2fU6Jh(;EX#%Z|q(+RKz94eo06A%^iVFd{+=2S5T8sUanF0vkxUk;kx6x z)s;*utG2UPG<7oO|Elw+T3YjSd*t;O`&Wzs)3neG)T_t}bF`@P%KWQH<^}9XPsF4F zH7gvPnB*^n4!zu70CdF@S|Jubc5`8@MJZz*$CEW0NL|M9v6|D1w@ZJB-b3{cVcozW zJ3YKiWmSJlpRY8YVL$e9bz4N5s7}5|7GT!2xS8GywKXx}#5CdM#n->hLLK_M`Z=m} zcqumiU2XX|I`^XExO5_L!eI*T|1?>X*o=S>>DP+cik`miZy4<20@*rNl14=>B_%c0 zhtyZEtFNs61r7D5)X6w$lkfZw79b2Q>WzF4#B-(W>e6XY*vqa?j|fgT_@FQAFAz0a8vi#QJEKd3{<-pOt0)aCEfvYKnn5blsUk6F zrRU7?)zgw~oferU4Hs9~^CiI%oeyw1yO%?1k1(-&MdT3~XPxrF&`=9;2P(Pvhrh*T zaMoCipF#3?81skG{72h53U8wo+QAHTG(W+B!jSI!-$Cm;ugSppDS3+MH8z=CkctR8 z9poLi>GN_99e9)CwB4nwYqx2k`QQk_8wa<$$IQ8sS2Ta33GN$F+%n^rU){b<{Pcv) z*!%sO8f>%G<@Os20z?LGukt^mP5NJ}FMKw5EaaDRRb7>Eao34V-@oVJ&3tM^0cLeM zo#5jUg_uYkYn-6_O0RomO-*&R=AvRJSMhYo8-+WM#k2i+$TjDd3ZZ`TA=DHr= z5%+QM@vW9hK`sKhWwpb@Fs|=c)>J8^3g;n40Mu$K>=37#=Qw#>A^;e2!Q_pKjq0g> z>8>twDXVkS<>?Yafa^3sxdYDkL{1hF=R9pb1#D?s2b&U~>69t76zRn9l#C~KeZ2rU zu_1n&3RXMdI669YeV<-?IKvV!om;YgUOFXm7<*hcg%5X>wpv4>+qPbX%YTv$m`wJ*%AtL789f-xeg0Y7hf9tAucK^ z<@pLIY#%(pS!{$yxzLJ#(5F!%<#b#<6SMmgBLi%ZWE4hSQ{61Zl!!;rL%s0vy3_$| zz?v4tb>_Vh-%kglu{TZPY@^ND{I=VR@mBl0M*G+UHNcPQp#?%M7G_Z2^i++;uA-UZ zt%7QNQ8#Z=>|32qOBWu6LK@7?P|I4U>k5M7oaDm!E~3W z5UuRjz%E#Peyk8YBU8{6*svBM+<~&bXo>mh(=1R`#jbw%XY&6LZ>`yn#fy^LdR_*O#dpQ$B9wbnaCb|*?%Bvo)b9EUU#ki>q(xF&qP?KJv zu2120p;)KaJv}}8lrF1h3kuE&mSp6MEzalze>tHV(`nf#F_@o`h`pZp=5kLw$O2UX zVRY}DReS4V`&d1=Sfea^$fV71_B03|K4Ua6V{XpPPv?2vG?jQ;@g;oNDU|$&(I0I! zfIG)mUV{FK1<{PpP`}a6thd(OoPqd>x0sz?2h)$|IvI#ryaTfHy68btF1ll;S2H82 z>Yy?)UaF0E9}7%L?QMY%=n+NO8_-9p3u+i!On!vthfzHve-9Y{)zzkU1J{oqOJvQ4 zHmC&pE~&t}Ciylr*d4fm%Cr^A4pKY&D|0-?M*|lut`^i5gK3gYp`=4>cF)+F+&A5J z1_7S<)W&&~Rtc?p9oiBK@l)Mrc}8O=J4mg&o63o0f4 z_tO^J3`L2!sJdezKyD{^Fkg07s+HIBmxPFc0n4hcev*>@)?A*y@UvGN;q;uzQtn4bs0Yw9#`2(45_OM|{Lx5JQ=BC{LitSO6OP{l%)OJR3p4^yg}BukoW{ z9h334x!+$MS+f7v;Kv#58c7IN7v<0Veo!_zQev$p=i>YCe zQ8mzcu8j+Z>J{`vYiyp;ioK~bERrBqb5}V+)M^FmR@3`km=4(^f2q}CC zjBoavc|5L8d5Ko3tA1*pP!6yea>8q5?T#fA5Pvs=MdpCY8LVxdfR_rooa%K z|Du_*wasev&*@1d2UJN)SHRaWK)svT-_R(HVU#_EXP|cpI+bal{z61SG*%=B&T6($dkO$E{ZH>H*b}a`6{;V!=P;XcKo^P?n)6Q?(+YcW**VyAo0l4> zceE?MUv-StkX497Qe)-fxwKq1GwuP#_SUc3A6z-uC99#eMGGg#mDwWSmn|I%I5f@A zUE0G?Z+a3Fn+p^;0%->_5qMFQNuO8A? z%(c|K%t-6Ja!VAftEyLbvFPY&{iM+%W+qZ$RF_R^Mq-4alqejp`Qm+e@-JqY-*dD5 z25RJ&uVl|_O9XAbH&*9zG;H*)U^aL@7$in~@w#jp#?2oJ_{P+Hn_aCYG=tTYm3mAE zX~-%T=AE~6TX*6??|}u6?UN^k4 zqG)ZJwc*K&?61{vm>&ZV!w2Db100Us1a`wi#A#}fYFhMX=ir~C{#7Ya54Xjy@ZH>y z20B28pDI{bW^aTDK0dlc*<=4+hX-AApj3~3j6**0DoYGAEpZ%)aNwf z<=R=bW#O(fw6ewVRsPJXI_z48ch>;3PAt<)3DbOV>1xvEqZzXS;TS**q1T>GgwCnS z;qS5hn1grE7O-~=5?S@Pk}KfryA6{Cu|W^Nd!ygX!Lb6qd|_>3o&Em97byD)cjsla z(Dvz4Qk~QGetrrj;3PrbT(!As8?=Etd~iZfX~x!wd-G~!KPUn%LoK-yTa%OH^(i~D zBJ+#stZPC{wQ%jbqIYif0+g-}4E$AYkm<}97Dw4y+*4Wo#|h<0LdF9#Qz?EVkBDnM zH#2Cxd>Fkqt8L1+oIJMaFfF)@%DTQZ#re1Gwm8FqC3|mCY<#wMXkcCLuUzr;6Ad)8 z@gkmxFBviM#E6_~M)XSAWYtubx`q8j?k&rc(Y>4RDw||?Q;ix3l(_U=-#myx&RwtX z<_|&g%h_32BFQJc(PJ7w(uvF=&uIN_r!G0eRE?aRP6(U1o@?77NsUQ&E6t#_5iq1m zXB9a3RxaXd7Z?yAXt^}O_2gwn0*kgsKkz!64I-ctOMwoN4Mozw4^fQe*Jpx`d6@>oj1OEWa9}k-|ifr!~J04R+7+3 z`%KYhu7+yB}sc|jPeQ)Ypnb!u%@ggZGP}H(S z8v!J5zAcp=)im6sBErEwgu5AeS(RNd>ccp@(RX%z-MN6KCg-+Fs8_2{b4&NVx#rDP zV$+B%sLtVvHXr?w1N+-7gainSTfClT$NIOLW^~)2CJE#IvI=%&1lJN-2WY!|l{@V7 zI`DBZRxUoQUqBaa>-0sO{)x3v+G^F7J@nfsS@>fPgUkBH3hpce5p z4sPVqxl>UvnoxSFEPR88sOLaG{f~0{;d&oh7p1{Fx4zt?HrffrySr=UyMl(DW)7U=ObDgB&OP5*TcQyVDv z4XQ0!+GIDN4>c@(X}s=A+W{(3g>|>_v^_Eav-4}AY z9UW@{Ac<;=k;ms+i{fqSOjD|AQ5}tzr|=`yCH{a|B&R;@N>ndkrys5SK-?pcOLLX3?JF*9|KDP0m)sEW(egmtFhw$1o6U)5BHfxOVMk z(v!X|L52@^R=~n);b-m0%n%i-2@D8|+?#o|o3qFT^bJErF;KL-yqn|Z_4Pz@Xd+s~ z$>}CP9aszyrc44YXAH$g`)=ZOGrI5Ko?PyAAzn=D@09?AT5^M?Z;F!pXPZ#0x*hG3 z!f+x2nxgc~A{OoE@UN(aJa+Z~_PR(fy__%MAWOH-$x~Tp`Y%#e2UP!yM4qB^MHC@LT zT5l^V0Ef+$y;8b=NeJK7cL->?#7Gn z^)e_f?kx5itJQ@IDK-RLVYi6#KM|#ypPXbrD9PP1u5p?*vLRJ0y6hN>h1#O!BKo>S zZa#J-O_^F)xj3SI^@i>=d%&?(FkWKzeZuEd)L)DbN0FXe z>oyI6s!3U=aWN65%YB8S7?y!*rvjDwy@B_oj*eqg^Oa@SZ)M)b(^Uetb>e!zIh z!L@~u@-+5cTKsi;|?CA3)r(t7O{b=w6nOS}4+Ng%6 z&X%@BsJdShe|K#V81@9;fqh0(M5eT{6)&MN9!J@CXT zE(vjZ@1GLr^&QlSVUwHE85~p}MQEiAWNtodzcz~DPQ=zsj(4v%kcE06CPfyb9Tr+8 z9QTN2-0k)!qwmcL+I%yv)M>K30~}~z3WL^lj-a~%;zi1@Fd-44k<{n&5>E=}mfs)? zVvrY?5+@#XGFfma64JuO9wkyB9s0`aj_z1G=0dJ&xTsL|G^k5i3xRXFXVrxu&bsG1 z@xX)aae3QjpZ!aglo&$j&Rm&SH&09~qpO4Whw*A<>CX^F9kL?5BHvs~_NVAeQ&$U* z1PVi-LGL?E8*h{8D==9Om6WS7x zmkvP6ZIcTk9#6I_6)d2;kFOTpCo$4E)BV)==Sc@rLC0Hk&K;b_toXz5r{j;7#-ZUM z`^CnBj2aHM6VO>H<%$R`TKgV@(zxsbjc_CJ4^Bf;;#8o?Q*s8~7t}IT*`0QDc1^yT zfPMGp$ycJMZN-kvxV3$Nf5bUteggJ~SY3TFu3@8JH|q8aJ6!NmHZubjlRPJttgZ4N zyRh;`7{u)d`ZuznsL1Moio3wRc(!K%ap__zSL2X~4;%IdVBA{Ie)FY^&NwUg{o+bD zf^XR(T2sBF751kC`m}6pgSVhbRK9rp-@-QYkAPVT5DaxKoF-D+)L9mt0;tj?9x)1S zD^#wMs*Wwo5GWGzL6kBa7OyL6KhyGwT_jHewk)GDEUkX|)?=00%4oKksREEld*joT z>ECZ-9wmjtxuo@vGI`^SwUn;%tFb^jjL*bMIJD-`GpkOU4X$lm}TGQB53B9g{~mETX0n=*7istM-fwuayAJ3CJ1yTCzuO7{(coa*(=ybi((3Le z*HZzx1RYL>1V=W424F_ybSRJR^~-C1y_j|@M^(M~nv6P4b>fpW$lBO2m%(kTmb&Nf zkDi5oS5IDvABcP5xR>h{)qdXi=qew7m;!l$q!n}3E_Pn>UFi8ae?8;MJu|O(ny;34 zR?UWn|H$Df1a-NjVpZu}ncS=w>xzdy$*GV+oiKq6q6t zov{TTi{mA4(0o~4vuyXAp(#TnIYcO@i+TJkmo5iq6sBt5C9hDY3Z}H$4%0^_&b+Xn zKZVErx!3u#GY|zQD=T0ix<76tQu{S5Ow{ArWzldJCQw!ZP1!w{2#4_4?IABU7*%r} zor*Q9}Mc?zfZQh=>Vq+d}P))9;f)(h@P z1b1TOP>nAC&Yuh!8rV6N*Zc z-a8~B0s_)P2LUOe1&H(xDj*%COAAs$?>&K#{ouELYt5P2Yt7#KtTSiM%<(S=$eZ_l zpLXBZecjj9`6yXKUjEld*n>(bK#PYi8PV?>>lEcLSG7o*LU}2V#7&%;B;UJ8c35kB|NR@s`3YQ`}{bLa<^Hb(*tx8 zFq6(8^(}ag3ISJTs_di$I-4-w9-bm;?6x9S@n#p4C%%1qP%^?zp;6D)H#|wgb;laI zeL#tGh;@`)9m`&Lx^_V%dVzCkxOk{!Y4*a$RHmZ>sT1+FpmpQ!+eev9M(z5Sb%@+s zGB2VD={KaVHqt4$uZldj`A(TPQDF;JMJ8xoz8IF0F^SE4f#Tc#RpsW2A251|QxGIm ze7!2<;L=#c04lR=FA z$MH{VAV9;UEwQ_4P)Fzmnle8T(}*C|_wNH0(rtRbnGlFUDu=ROZg%6VNA_a!eH%yD zW|Rz54MMyLd;0dd>)@PnDF==Amci(QjApX?YKf&QZit}b} zPNZW++-F9_dMQ8cB7%qcIh!12s?PfRD)UyjrN@41W$?NMe4jzG$k;so<78g1n%)vr z;0om>I$__`u&}UcGER_7K6QCBoC`^z0}`q^y~Zc|LD%#-jjNU;0MSA)@|R{t8z}zZ zetsRM!5BkNA=#sdq`_L}S`jG{(|E8A8SDg`1N&PBkYH*^PrP=oPK7Ow$XI;@%!_+Z z%sWC7x6&L=^p`O&PPHURZU#~6QPb1Y>FK=~0ik2aH9%NX4KLk2&JUZbG0EvAJ+Vu2 zA-$g;Buf`dRfPQQD;?f&q}kTIn=>EpMkD`K9yai-|v>2qX}*xn6j>1 z%AeA>($Gx&(oD>1Q@=}L)6)2cTVf!~X=m@MeC{<$AF_Z!ezT*GvD33DLCpzLj_*sO zWp=oUWs)W85IrrdG|6syOJCIXQ*dy9`awj8m`5#5KHr2IrW|M!0~`=+)Y_n&XWFKI zc;Z)n>FrgHbg#gFDYu1Nzm=jbS!z}Q7>j=Q`+YVuHR;-X=)HldzfswA9Cl_(@j_VR zaCM0K%341r1D*MUP@K4`$HySzxvxBc$*A>LuUpb_u^bQxD4k8B+DiMxY54%_!6w@o z+(dxz8u;pGIGjj(I^Pq&do*DK`4X(uNXYuOA?Xp5kW9`BBt5>>i%EIS(4c#?GhZjh zma^nF%5#FkKPc>Y&d=aRvW>Uq^4z7DRIJ?XQ{Xg{0onG~;*QD5rzD6sd15et>*07g za0{+W>7zvM!uT&N0Q%l0@dDkquNT$&46PXs=t*nRpOevw-|9rJ-jhO4X|TY7E-JV- zuj>g0h@$$o!h9MhFKN-*J!)FDdA%N)W0V4@3!CAfvv6xs$|_?!ozu8c{ix-)yH26c zQlFF$Ye30)4n)L}B6}dHz$~HT!T|K=G2O98xim1>hFk*j0oErI`Ak=dwXR6rmDR;f z<;JwRC+|+~>-7ud8B`V|IM4Ufa9r*Lwdc-a>UQyvEl1_)x>sA2a&ku+3p|)(lhSRy zwq*WsK)Pk}u@csWYd8r!KZ#L|zSEj0?w$vV{gF4o?WM(nSOP%rPnTtLc>;bVhr ziZ{6LpW$N?nCsXZA9o`i44ZMp4w|p6t)5KmS8n&v`KW7ki~-3%x)rxkb~*2(*8w{> zyVFLnJ^O@ivD$qGg~cF@q?Qu%;7s({mLG!N9jrO%0Z~kbYN+Qm^{GIum?j?xbf&A;)e+i@h-ypd1I(@xBAOH{@foi=Kco>Y%Pvm3zwfZ~qXSmzPps`fne?4Sk4w`&n*e zqTlo%jk+d5*ZHqCyu@l$SW01iDQrwB-N?Njb5A29 z^9EUaaHey}=cfpqzv42~Rx@iVrSt*zs*Q$OMX|epD$dUQ73%Z>?zgCv`nI@QV7W{o+x+KXqK=s;_lJ_y|CS*4RE{! z)L}wHLqRU8sR^18Tg#sb2?;F?Hi^>JJQOMsicUy>gwt`g};D2 z2?DfrYPP9G=G4?wtHGGf<7|wtHFP%i1IH^^zkR{8GiT1^sH9w?qSC@X-!|?%y*tUG zUJ)|I2Ljf^1^ut=?6~{5ZO6*`8E)Kg!FA9Cdj?#hsd3*(jgOBPbqcw`mi69aeWDr| zQn#B!(_$VmyUNqT2e-Y|Bh|pj|b-1i!>p3u9@gjqP zMvZVoY!9HB^W`_UeE9I8xhwDm3x|oqGhHWh=}wvgMuug|X58{}5}%pE#fulEq@*l) z^Qh6pA&L&lw#&0ZF|UwtTtKlo4BIj}X}AX8o(txT7kZ>qWU!rE@v$wDKc{XMr5OJS zP+iJi&37e&L%S@=t-@y1l!$Hc70XLHoerQm$hqMHi00;k!%d|izE+jsA0<)}8I@(8 z8@4(>&oZkh3UyKM=;={G4yA~>GvVbwmlqeS%SE45J1@k2alvn!1;0Rctgfx?O=-SR zGd>A-&RygVh&_l+j7hzEPcr6f9J^Nj^PWHjr<6HJJIX3{=&KX$eTY_nHj`<45D31u zjmCzm`JQag0S8V76jSvfo&0;w=yy^L)5QpT$X%%d|tkI=8hXefIbEMGtrC}hNoZdzdzpBLbe@G6Yc>^? z$IATavfXUQKRHvSmM#sY7~Ei7n(0R=;Z_(n7S@qd z*j&4I4SMLiyBNiEFW5nUR4wIRxfT1U5_SCk`f_mXcCKXXx-LH_MqzBgtUOd7B@-GD zXdzRA8Wb{1&qA^-X@jokfZth&XG1^WkIHi3;<`^C+-$z2jr32{$SKGI}|U?k%c;bx8-2WR*KmGbD}5=nS(H9aAfZAD+B% zMx{<9CyYjxX=lk_E4L^(YcJht_V&H02-$;;Tx_fYJBm>LQ}_BHV!Gb@psf6OeMK?l z!wvY5V!=@KP_${1mE277Kl6Hd_T0-gZiQkdO)+29t)_tiez20MZ>6N9V2tYXr5OW7z(}LjbsjeUaPCK zww%_%K_T+!k^I_~%a;eFY;`-*ixmb!)+tSygXbSV03(vk!YD7Haph54|5D@QbQ}~S z)|}!=Pfu@orx2Q-a{G9?y+nSo-eLNk_z!(8*EUfVX8Eo7T7L&A|Lo~vru-usxuO!XU}`3V5A(+J!u`Um|V~$1Gcqql9Q7=$1NvA)^$QnQ|+;JY)d+KQLtLta(1jxOzN_Y;)m`8 z-b@$Pd7F37h5nf=oeMs<-gNt4yvk?R5yN9FnfJHCNwvpThLC(}$9+}RUR|E3{@ldG zM5cS~-h6{oQ^tkd3d(yuDPr5m0=-g&J>Bd(9BO&^&9IK@z=g*~m=r24dHD~N$ioF3 zj#*2Yyb}dn9hcOz^ROAK$M_vM93F930-1CE@=Z-uO6`sQ<5YRWkOqYDo+ z{0w%#^<$<-AOGfBZ~w$p>3H%1Ta+&UhakJqoJ;-%CBh)o+;FFolqnYx)7$qSzjD0w=Jb!q{!?*XAIUSnfLyUgwvvj9iUKuc zn*uK1j+mEP`e)x_BfN3<Eo74zoY3;qjnUR_3uaqk}Qp+u90t zKSxiW49<2K40c2yutOfSvnI^(nRgwoj`AZAq$+>PuDa6`|7S~!Y?1&w`|{z|Y)38c zWBLnBuXfD4lZL>#fTj0heLgRuoFrfc?3h%k?BU=j0!ypbb0-@)SbDk^e#;P^ocT2| zF#%5CCr@;17#JAH&z_~mvtPd+{XVecIS3PI=;-p9qL|WNA{A3a%g>d1MCbS=#@C>m zRf2tx@Yn^<-)0m_^=Uj2sOO57(yV zN*|3x)^fhPa}XsPX=$~V_EZ0qK|#h;9-;OHDZu>dsdRw2!eU8TS=q2UHtu~9t0+>G z>7ykP_0Yn^gwfvRV14oGKU5Ogs z6PoV^$5%Vy`nf{|`T3*q@r3-YU{jT)GXNq{B6c}CYfUg}3hKZ9C=4|aO9@u)0n z)t2*nQ*{O2uwi6mSV^bK&(p)ki(R{foC={J%J6!E2tButInnK&P^U81Hwd& zYv<;jj*snVQ<>QPD0$-&)Ar}7zinR%&3X6{8v675EBNF|U8E{&z0-veIVjm9_!ath z^e%caKh)}`7a~hCFZDZn#IhD9xH@%!tetD@9P_Ouof}l$iD@8W|CI=}+SXu4L>xnNPV$_Aj>HEKr!4xZP z@B|*E{PI)ZkgspAGQ2fwG=};yooT>GcUC3?lw6ps z^D6z3Wwac2mX-NXReEjg)uz&PL@lQtH``yV=1E{G4rDmZ*jMejbtnJ4ua}|gPwjjt zV-7>mUPn-U@f&p*=p!`OB;tegPBVeGQAa<|=t^Un4cjk-hlg)#Dug4ez+#e0C;L>! zXVJsC_cX6N|JoyP*mbnNd?|#jd0B*@FN|QebMws<)34uCoKczX%ap@N?9rV*hxcB( z{X5*+8u4(;B#+yuFRIFQ_D7iYNDnUs<;&3-^*7Hl zvx6*&DAD}_8J!sK&pD>S=Aw9yc+E6wHnk5VC0U4&61S#&_e3gUu)Lq4n8@*Ddk1pp z7CNWk8Y^o~Rj7GpCirr)7fXy2=k9yD_G`3>J79xbv{&z^<1+_=hw1*r7=V}pnXuAyy{W6K00h1Pmv6=C4Q=qOa2PlEB=VW{Z?J6`>9SJH z3WMj^q`?0r{0;du!-$NH0STXaWGIhUCoO@flz4m$=Kaj-YQcWgC&p(gr|0>nHnD>r zu|LmU-=UYXUj}1AZFuP>XKpI<(TmD+-kZPunC`TTK-Z&L>cQ~@>#g$}jbxi@@P#>j zK4+q5%qK8xf4NnR{(?{2e5@t5+F8!rX#Oz}Wv(%)4;7D{NEWo#7JQly(|r2$ZBC8_ z$g8ph!LEHg+^ANlj!=ipKf}t8sp)eL3x2g%u4fWckUN#duG-IyavVHLV?99v2?es> z&aY?Y;rPyHlXW z>~+BZ;@*Scc9eNE;rBRjzGbhZTRCUy^@RIQ&(m_`zRl~-2t8vGV-5)hI&&kI2K-g;r3gW_OlK^Yrr(-6tD7m z?l^#P>9n%~@Z8z2i;4dz#`^;t?gn?oO&0STvEY=8w2>!JU5VI-&(AI#DFL4qSvk4Z z@u`0|wlWp|wtcX}%gakq>MkHS`$HdGSe`p;pZ@l5Il}qROlyXpan=F$H%}+x)9q$_ z$LW9mPo|_a^yJEy6#wY@`snI_fY;3rzc-Pe^&uzvQhk(>d001nR{Krq`f5(mMe|`N)I+3l9EYx1CW4KX0yvf?3Hf@w!XJ?Uc#0dh?H(jZ0oez z*Q^m1jL&-v_2bYd*m}9O^~qA0i_0!g>3qKG2DUy2yb!XqB-3keNr%pv5g5!^y;XOO z$;AT~e2vBioSeod9C&vyDX_g*8qnV-?cW=$885e^rKPDRRPF^D&nzzfURc1KUZfog z{E`m)Od@7kRcP1@)K)aA)chbMdgtx7h9(k`-)HeYNb1WG;j5$Jt0cJ}t9^w#sfMtg zH1Xrk7>XVh6@ms@H+|xrNTb0MU(=m5YsGl>Yb*8rSt^32FA<(=qq7$mdQ;ceQ;p~a zUQOhR@Mp)d89EK5HBL5qws&~aaLN;vr*ps-Sf0N;N3x#G=U7ltO4%(92$1c$GaAqR z$$9NN#BzsjDF4lXIt+I6nGt-et8b{0n^59NPJS8IXjj;>W6_>KvYGl-q}=)bh4tvF zq8_)HSnX1DYnq7L%hq`*Q$155YPzYfGi_O>JHYHS325org!F&pW78Nol#;YBTYU%{ z=G>;%bN41;$MSsXaRX88W(xRopi>7jhF{=qGTS+HU%0vFpDol78oHC=$#0ftmSWx$ z(>FZqw*9_%+c8>bb;nTn6n8uPf|Nb~ z&z*{WZf)&2#hYC!Y21_ZwqmPmt4zFcJUZF0%g2n2RLydelhNl^J^6DctKIFl`f-OK&21+9c*qRiC%j`3NZY2sTk z`JXfWo4$MUXEid}?gjL}rJa}rYLIL}{C6e22CJUcU%Yx3;!y8=fSobpm|LnF9YBv* zIqVGEy3Q%l@)sC2EDkO?Dh)7ZeyuNBZ*q0TDS)OMsC{YJENs` zQ+VJZkC+YI8>_T*J^9|!Bp1%cLx-5io!%bR+Z*@7+l13eJ{M{PC?6NBxW;Qpft;}4 zK>9sRkPM3+ZJg@LG5ZaG^q0Zlb|nhE)fh!z9V6?&x9I1xXA)v9AVYS{gnn4Tw-IZv>9J&BoE4hb@)U149n(5(q4o` z=Xme08&`TI-YHd+`5Yt9$ zKz5;Crd7f9I{V26t`&Jvl~3Z#vL~VOMT5BmYN5TG)0p8_0*a z9;tYCEv|D%utBTj-=*++r)n!$vz; z^fucgt&GDsTd~1ij=SO>9)vDE+gwmcX+|1|I42}Do8})K+;brnVJd!KkY7`wd!P|W z2{58Znx>C~Ep%8Dks`jm5hgfN)+&Q_G9=gyD0d!vTnxr9c>7JjTw^D&` zZoZoMD~3`}Pfx)kn!O0#Df zOqURIU&G=L?Dqclr#9Z{cy=kVz-_45V|6+`Mf5HW?Z7m~-J8i0M&v+)b{}p7FVCqB zv<`-Y*L840F8uCDTH3Ma&ijA>)!SmrW<1dew+k8}%}$G(>O|ps*A29!L z3eq>a8|rzew)SCA@ZAZq7$8jD(L~hV>G9OX6ne>_-AT)No+l3dl$Wg*r5fKe+@rmQ zG4i9mdlNWqW)mUPi(S<_n3U|nt2Blm`!<#q77l!J3kz4j$-R(WR>jYtIAE0G?*z&Br-@jiHQ;U1n4YYgM*BEYLk!(PBj(gCn5C1swFzU|=eJ+K4(36*5{^1Q>N(?nDZDL$ zuV;z_S%J>%F4=$H+lA(2Q)l609NPf zu8JU?kndRz7bGexDGj>!xlhlSwHrp~bXU0%1CdYqZxBFfrA2Vv9?yiIVL}$v%0);F znp{faLn%ILPwm1JFJ9fA>lylNlyaR_Rh7s*uXM>0CR-VPYSwjMPr*paKMVbB-Dcm6 zaqr`>_rX1|ZY&3XkGt*yq6vLLbnO!{H1JjG-s5rRijTl-o1V0_e)Hj|F)Te^<*JT zKqH=G_=!bt>kZPLFa;!{-rFr&)Kof}D@S-#wA@O8N@U%1%GdEV@kD#3y=$;YPX^{r zy`gx6Bj4vz&nJnglF|4?KGVfX`^b!=mI9%|q|V|CYr^#)b$dIA zquP77{-|@nPwGmVnDcVNHJK4o^*3x!kw#EOEQi*w5?k-rEP_s}hZM6fl#e}kYotz` z+}!ARUB1lSv0bd%idKy0EcQ4lpe>!haPESF!18Ee=hp{Kv8J3BJ$DLWLIWG67E>od zSMMn&h}$+1hJdDXLgkxa3?_~9Lt0u|X44M8IuM#2tFTIC*Xn3dj8i;cB~v>L#ZJ__ zr%@tB^Y56w=_?4XJScU(#oa9GNEqc$v(jQ$^p8k;GPII+a7!>B5|_&7n=R@BbV4EH zDtD#a);16d`Nx)RPstY^G{$v(y-M_!d;Vgg$`kE1#LgFe`6f?vhg(4(w_!t#$I^C> zaXDgsXBEuKM1Hen649NS?iM#*Fr6))TdU4~<1S(Y3(HdTYpC~5756nCi1SdG+9-Jf z;{9U~mJeBvT&3d!;uf>4%CWAMFV}f?ZgKDJ9FU5eH(+#FTBXK z79qVQ6Y)`djA+<5cGo)H%jeif&;Le`6e&tts$qIS!em(OW!qW7Kz`xsQMN`<_}aMq zW{9(NISk}DRLL=7R4S{IsKDuEzAbPmUBC4VU)5l`^hhaRn;p2jmw> z0&_tDJLdX&`U+t#`*i^W=3VMRMf#;GNtK4l+FZ#9w(g^y!*=}kJe&R;*9LZXvoEuK zz(OT?SBfGHDs4agrvX8w?UA?thOniJIor@V4S(r34d^PStld{vFhhJ?v- zGaP88CyM(VE*_A>HCM33b)7TMpFa-_3JL-p&-NZ!1Cf9ulSFz~Y@l&&Opz-_%<&Y+P9QCQ2nmr$V z8l2o6XbiQPG3)w$vREu9UFibi2{%VK=^nMy9Fz=*8S@IwGm&mA%Uk*NM`+>)URjJK;+@o|83- zfn|cs=|n>(G4K_e-&M#QksM2+MkQtk-9a(eUT9}mlO zFPScmyRAa8{-p~&oqxOl$wdDfTyq})sOq^#LSA0tSxXI#(hr-P?*seQJnnroMf2DF z8!U74Ejf``U~3cX)_=DDir*PHMcCheKb3h9oCh;}`qo~W@f`dnfO@1(tF~`n%_~p` zkK%>YS65TIxHxYxvf%Q*l1-73-e-Pj!~{#rJOI5*hA+RvQfi*Tm;d8qr_#KCd;-ak z`u|sd#EEmKJemK>cKv@H==o2p#sBFg9_t`us;dM1qeElM3qf`N50oPw6&3a1o8Jk% z_aWoM8)WihWH#pJ>q+dkZ!z``YA;!$?Q4(=7b0D9o+c;i z$rJWGmuatEdqDuX{2y}PwQJKQ{>yQq^@*O;Y52c>Ys3JD+i&foQ@uK;^8-nXkh*tK z?$$r9#5T4j^xeVC;OFx2hm4H>#u{AJ{F|SDXL0e(DLw#9Y%??d`NbhE6ZpaZmXG~^ z`ZoS2Mir;m{y#w)&;CJM|KA$c|Aq%SoH*+MiYUuN7<&_wOdlg?wi3RPVpcJpyKpa0 zzkIphIDQ?wW8bM=7;}*46}EgzFd3*N_z}b9@8hA>4&nQ3YAe8OG(S$5%Q&^BE&s+H$I(~-wYZ_MjmEgWqZB1 zy&aOBNo*HJ*4O)QW0|Hc0OI;|uloG&@D$OGA|C(*YzIf{!irzZu32{TynZ4ony+d- zR3u?GrrFDCgw`n zkSC+xq^=ZAeQwy+)~~SXvwzz7GxDcL&`mZ2)2?5<+$udC7Wq(|0+)V;Cf3jIhwwy2 zuuioTjo#(4sR{hfiP+{;WV%eu0b`zIGMH#|&bS?D-SgmRR@E1BavW)V!71bysXjf1 zzZ1tz%@yLNI0+_uEVcSlJ1Ins6nhCd!rze`01lf^OEfwN;!GIxp;vh z%OiLu(S=pXyfbbsRKI+t@KH@ocCL24zTa71ty-l-{&vOJVUeJ?=V@DHP`&G&t&&os zG+KhBTw7!4(8;}z8XHN?0_+Bj3WI7ubJ2srYEdrH@#q{z1;mn@n_C=2D-VitNdF#i z>890iU1D%JWq({tdmm)~2`VB60l0xa3q1@=fPfOVf0K_SXg#didyNZPCSQO9%m)tL zFQ2;#V`EWc3;4n1K~P>*PGom&@WIdYRMc(or&$0R%>bbdZ7QmT)>e*s7=S{dlpbj1 zYmTXnUgps%i4pj~nLXAD$TL6KU=<0y*wWO}Hc{A;eOXBbZdJ{F zeZ1znXYJWTIYXD0=RvPVw|#3kLxiAtSdt*0X;&`O!^@+;#yaE*9HJgtiJ`aWx?vfn z!MY|LWAS}uIegJ2oZGk1xFMduPl1h=(DsU$pGWy=oO-`JFm*pkb-BrvWv>bxO}(SF zctST;7FZc`S-sxCN?Zi2CgjaCU6%TbE8JY)Z9h|+5z}TaH7wDDFgH}h0!=Wgw5SKh zH%i|8Vf&`--m;~3e$NZw7wz}r*hY8z@wmce*ka>;VWW`o(P)fnSV3q;OGt--rNk^e zw4=7|7!7;ZRace@&auxsdRlMy6dg)rDqc>1ZwDmi0^X8HJA{0Q{p^o$!%DXl(UR4d z%AW)v`;KO+D*H9soW@Zi&McP|J7;;^uA9j%w?2?zD(JXuCTBJ9BdjU#ipItAjh_t) zL;3s`UAIN7VpfCNn>Vo40~xZ4d1@^w9G+{RzxmS(TXHgNzW_V|3AZ`j6`?{P`pY9@ zmgBa(YsqIab+BS#Vk==a*VaHJz*iY5YgY6ZT;Ul=85!F8{8%GRpzf z&lpzKyS7%P=Dqqy<71aW7G&42b6JjxUT{_F<=eS)d89)EUL~1NOaHUX_&(r4MX@1> zdm|~0YY%tsdw&9^rI~VJ6D6LC{()4D8%Om7C(qh@gsYJ$=1XyO_Txgf1qFpDWm8qJ zW70__e3H=Ig`C#n_gejBty46d2dk9rylJ&NSz((j@L_+9#aJlnW}G3yvoIo}muDac z2y+FSunwkUY$4>7pM3Arf4uP(tyxgurh`#8nSK{g0v{r77a2ChlG8Q?UX316NCO*q zh4na#fB@&Zi|JC9ya!>B4d+TfO>dh45!Bmrfbl7I=Y$R*vYg#UTdBvuIidmBj zw2Rl`fAdB3kv6fTov&B-?-jpJ25Jyws?F?!;*`YR-sy#v6B-3{GWIoRlIEBtT>&6q}P;Y2j<5aH+;;z&ypPX<8332gV z*8WBW=~B%K1D&wlM6P<~vKBg@L-vYOyZbvz9NR z$-}lWIuTls{Ugi64<)#ceAeJ*D;OTTI8=MEYdJujKiW}4FX-jcHzQl?x|Adc8|Iz; zp`0da+r2!reC>x8hxT$r0lXts(icwnTI*v=cVg4=>K(;(R)t*c#$q0$s-KzO5n#@; zIX1}3h2M5uSWlr!7D^R$%|CFjfAR5-9v~3DprY^8L4lR^5ephQhIzi_9r=`Oa0FNS zjBfiL`S7r0Lv(SgJ2}8Y`J(fgeVR_o@oh1Ya%6b zUeuECIdK0A`a|*=;e{!-Q4eLu%B-}|=7p1_)i|qHY=*_mH%I-F1w5U)^#w0d-FXyT z`J#Mz-rT%!ykgi&&#Jpxe{Wgl0!cUB0Iy6h+&*Dc?IepWcK9ZZgwxa$bs+xzz*a~3 zU>zHK1s`3-Et?A1Eb>aRQD%Ck3--;qQPj_d801DTGKhGYCRfi-0qol`xJav4^m+Winv|uF#R1Im+eaHC*FX+vQ{vt zT?lhuBCLJ(^EYM7*C}+vcQEh0`(dUS51*=ya1pQ{)5MxB^rS3v3fc|ozTk#sD=pYJ zRQn#erEMIwEqknS(i#aaQ^s>@El>Mj6hw~zRmZ(TSf$laK1yp}6g~tw`|BP4vb83L zJM@eD2G-9j^ko$@RjBL!vrk+*ILC8Kam?lJU*f~k)%^ppSgcIom8i|(63YIjGNkx^p)ART<$z zkcv<$AUk(lkn$SL)!m?9i5{wYl?fCW+szA7dVVggU`&PP3L;zLZ5!m%zpJKsp*c`K)qqHt8XcD9*p#s0ck$rfqD};}>Hcw1 zYX}Vow{cC@qOA#((Xh+|5`IU-^hoFGf~75o@&m3>{CH2j*SuY=o3WZmSSq1 zr)b5>Bc3}4JERw3x7oT;&F;uXAJIAaIL^M}Ox`7eVS^AALJ0TvgO7=#^&lFi$|7lx z6&KMS+HE=TEU@qEP8L#poXYPVe#aq7bO*G}n!O>NfjE5o3joQKKIUf*%_vkRnUl6U zd7w2El$5^!lOB-sWTf8>@iwmPrFwr5Rh51IT)ub__}D;&M6x-M@(HwjE{mIK zaQ%9#*c%VCl!|@uPD_~7zMS=BY>lV$YC>^|Dw|e7z4yKXDe||+`XIE@Xz~&bhs#mL zxDaM0R?afV83b&`n|y{xIwTd^s!2m}JM5JrWh8UTG1LI$mK%_19*j^HI-xitE`bN%_OJl*Th? zDNQ#m?}K$Ntwl4%I4@7H^XJcVCp132`CM_E+CKcQ$V0^k;M7T*a@`m$_6_n_Fb)?S zJkHQ=y~41+Hhznan!UCM40lXMrNEK3Mf+yHx^b=J>IriY`QgYa&X6)r3#U z!AF>*-=4PVU#E%dB90@zHJo;*h>7?AYj~KK zm1zp$V0>2t<&{cr#?c+k+MBD+h?|{4tJ?EB?{FKtH@IwN)Hw`fhuz{a>WURx2*tB; zn~kRh63gI|UZeUmSRI~WR>dpjU1?&flGpnG;?unHYIk|a`nZ>*AuN=L=K{0r8-6vB zf15U<_C~QvxtdOh{<}Ea=R%aOde9X^yZhy4#>S1+r3;JpCg(5R&@K1cn{AB{E3+qU zPRkI3YiqJ~zr6AL2rB90AWeF-<>=;?+UVdX4j(Gk$b3O{w7)Wfu{T~5@^*U93%bp( zOyk;WlW^zrcBMH{>&dA$I5p$OynXu?B3Mv@QhV%IIJxXK~aj9z7zaE zCJ?`1BXxnNGdgMnsCIDbZfbNkeI1#7ccSs$6s*qX-)HgYxghKZT6T{0ypaK#n&(+inG`zC zJ^SC9^?Ui(D(o>jjj8+69M)(HY_E}kA_e3P;oc|R8FSI*mmohpTalwY?I(aT$xw~sN%i?4p15l_Jt2-25i@C|d%Vm~QAIRhn<_%69 zw%if~)tXA%@gmZ;K;FQcmc$ofbdb?yG^U~d-ZS>A?&}lkMytuG1F<)?z*!jW)CyAf z*WFN%A(mM5N}3RO2ogK%$@lBj2tooW$ZWN zp3QMr<-u{fUdXy3WIeCMoc_Xl($~E_or+Ej z0kEb{#%w5eDHm+NE5n!()h~8zlUdCRfL*h_%zIW(xmf!-U=H_ZD;|s(Z%s^98PyX2 z(*`DF-?x$YO~QYWd`QUWv-Vq8iz^39N4{^whn<;#k|qtI0IJ(3K+@9gTjx9RbaVzjTl@B8=Vypc|N zf3)1~cuu1_=LK$0{d)a9j2NQctW$gK`YW@GX1+kbyX2!CD{ z%>#VMrriFW57$oP6CQ^A|;-Zt!dUj{XJY!vJxwMbwqiSbJr21j_8!F zRLVRq#o!s{tgO7r=dCTw0j=n~>;avWvQF(37n+1t4AxQ3#Dm8eky~C4a=);gRu20? z{5Utk!X6{{j*RT8{%MZ@N&$~UNRc?ptEib3uh5ESk=~xMA`2HJ^>A}<|0*_Y^{|&W z@ra|uBzZ)5y%}<%PtgtiDQka?vNG${7WR((^_8rkzW?KZN*v_-h?e8TknyXv=h=jnxPeid2^?;U<9Egi@Vy1Ft{nA5ya z-grW@!HRZ_{sG)&>zt2(LLW>_48xi_c0W;n#BOq?o=n$sl9#8Oc*J|=S9f>eYgCBS zglYeK3NMXE4>Z=Xh2tNh80W?dp8B2DAB+j^yfv%CAl81-`S)i(E?9YpmhQ0pGuPoR zJ%7Xn3y+^4Z$_aj9-aGYp#!Drc|5YPzwP2->&Xmp!=!5w z8Cfq}IQG)=)hjSo#EP(0yZz&JF`TU9k1_W718;-LiLw!eynNSWp=Do}8KckWw364w zrJ?w!2p1l^5XJ@q!FrT`ppop!uu6JLO1%QsYs6bIrpj$E$!*!ZFGH3|tVJxPN@tm+>{>$~ zOOj@^@|#79$r9Zq?N29ek6M#~TkpYm($pZo$DY(*B@g?TeND4|j~3|dfY}o_VPLrJ(sElEbKIoTQuaK4#{B|~D4<+?V zsN&hRtd?d_Aw~O-jGqTwx-}dq1R``$a;7jO9RU zCjcgEReiPH-Ul|k@3m-`&>V7(5OQE6~0AJ9zt)nbD`kF2$x?AOygD`L$a($>5?wcdI} z{kT)a6j5fa6>BB6qbNlSRaJ}>=~%m)ZG-8^ zcLW`uSe=*?WU*G`+M)9Szr;wq{MKa16No*UjfkjX#Y--q%v>p=Fe2mJUz;3HH)(Ak z9<5C@$n3K*zYGctFsgA-pLC4L`XB9G*H_b98V#a|y>JmlMY#;3AO-{sT-sGE6a_&* zK@2EMAR;YDhfqWuyn-_#gd&Nw#K5HpF#$qx7!gS5U_uRJLQ4pV0%?ShIX=%{Fl*hH z^KjPsy`0~-zVq$9*WO1C$dH)UwjApfMMI39ky~C2$1DZ(uGc`G-Q&LRDN5)%S4Zt)ZzLH`4iwGsr1l= zCx$AQ>p7_5qn7B1C&AJe>I*Oga#~nz;)^Yb>DZySwm8mSCNs4xqpzVSFuHA}TPu;OKYhOY2rPitTVP^RQGUUGCR#Ke zXciP6@I!-9J5$Fij&a07-cr85O<*>4-abs_!5xdrfAq3Uh_s^P>dW z?Jp8{5g7s0HNv)rU8GR?>26@T#m{Sxd_XGt#hTy6bQ|Xsk$E z7*cODW6#m~{$lVz!ZVF~#Gr2-Q;4F8gbvA$rz-m#bJluXUGk)kU`8~1{C$w$8o$9C zrh&|a9QtR^YVU_)%{MmP$wQ@`ArmCT*!v+0E$?`D*F3qVXm#^Da^jlB#)kuhZzDdT zBe*^S9zz07r0ZVTQUunX%%6sZPME**RT$JZucYmQHZJv3kOG`2oKX`p`LT7GE^6xX zSB69!)7JS^;R31g+j=%+e++*V(KD=@brgF`xTr6VTFk#7Lv9aL*?U;Rq~^kyv20$N zg?K7bJX{g>gg2-ycE-Z*v)6WedoLyC4v(u$qwQA=2T-4;TB^MRogL$s0|0^_(f-p^To;0OCl0J8snO|0XE zpuv;NMR(dB#1+))%ekgVS$Ekh5oeMtaS^e|58pyIgdLG7dJr^uKY!%c_&N-!_u3yW z5NDNrnk~pV**<;&hd=<O9e(3#_C@)2Ps{a1))Ci^ z`WSSUmi7AD$Uwe%+;GZ8Wz@~4l@Ky z1IgGAc5*!R%ss8e6+8n@dm~hG^X36!xse8D=iS*+0m?movR7rpy+CE_I%VMKqHgG6u)Mr;)^2+>TRx!I1}O42_ha-XF0?Eej@D#4Bd#1 zWk+g;jkm-B6XOxc$?~#C^v(P8ev%;ve5kxDuUAnrs=dv{*6e}g`_#WrnI!aIw-k-} z(|R+Ka<7KkZc5n1i%sYmtctO|$NELS7ggPo-^gC6oULG<6CCA^5?03-uma1-gt#9%qDH$yds`ma!3YpB4<+5MdqPntchf@LrW_*WJ zdkdSRz{|#qE6hvVZk^8(E{{cS3lrTfZ1AX@or8a9vO;Wn61*$Yt~95+2c1Y-8$$W8 zPDf4CtM7`DQmA3I*O?5bnZFUz@9SjX`OTt2&pCOidjw+KAmTXtAYduw8@mPu3N3<% zF|Gd3@q*ClKivs#gFG9l!KF&ob*C$>{%Vd5@5Y_$El zu@3|W5@vwN8?!WFM$yhCb>p3}-ep&_=nObnoVxQSuF|9GiC`#FrZI^P)bw#6mEYkR4H-RXInfpX`O?|zm2Dw zv?m2fDYPnvIquu|9@yitLE*O=u7-K|l`EFuET-d^s=3pXx%P zf~oSJI8gOo?WpOaPBV8X(IElJ#ZOjxZj*xm*ZqRLxgJotv>PI$sTv(SxHnY=k|HRA zWZ1F7cl}3a!R8atq;D!I0VCq?s#`$PQ_B9WMqo=@)eo$f_J0Cv`pFako>ebGfd77f z+E1(E$*q5V@YgB*AD6~VW?A-c`jO@b{-?e>l0mI26y;Z|ekOdV{6yKxkTZU~ghM&* RHDCwgaK-s@%_YCI{{qRCT!a7s diff --git a/plugins/voyage/tests/e2e/snapshots/voyage-playground-light.png b/plugins/voyage/tests/e2e/snapshots/voyage-playground-light.png deleted file mode 100644 index ef928a4e3774ebfe5a4d77a7f1e105801e544cac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 95365 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%0A0*QY^Q0s%Z?pA+5Akx~Lte1ozkAZ`szz(P zIbedmgcVVCJ$WT#hr28fo4gvaWd$-GmEY zov&2?9QWU2%FvnooXT#)Y&aZ~SNxr@TX0unWO_q&i{z8}AZizzf5xUFexwej!{e=ZeQ{ZsjuRMq zNnq%AE{V!Dz;sI-xooQQajDz|>E$TM;oTMd_I?WoH+KQ=hJ`?#5kt=L)kzyT`#I<@qE%{Pf z^a%?$3*6&A#D>}F?N)Dug1YxeXmj(^*qZYR`C1p`>lfa5OQS?=4&T<> ze{Nx<5*$DJz#?X9*^GUrNuMv!nF{gQa(cI}%qZ{T!s%p4xfW(@w5Oh|Yh;v%tSLiF z#i=T+t7}J2gOP+j&BnUDgB2(+#DG{P@c#X;$z`9 zv&#`IZ*-DqI?ov16kNE-2LMiWTVRN!g;^2Kk&H4Csc0+@qmSw&oznI@$@5tfj;F?A zr^|*n+z=56kWsCtJt-ogx!v1S3E{;_s4IJKVez|95K34X{4MatccIksns!olo(p`{-MpRY zeplSxm?~nVd3LEUo#xRJcA4v9U8LeXzVnB*N*B8)tM#OQ*liwvgTeLP&H1adIQd}Z zZw3Gyo7Ft_F}nWU{sXBHo|kDykKW!ttj%>$%#?lVF&)8i-$hUunXKSh-<7o;S_~c)4Og^ox?WE; zicsv>bGd{(f3Ky}iEyfFe`|OHlWwSB#yL}*B(*$2T=(@y@7s&D+;_!#kO*g+=@TV| zXFq;chUjtA7jrmzM^(-X#|}nP!0lPH!|`aGHQBi zrf#vwmiI+l=L51@hy?U;(jx+Yr@Q;XV}~j5Cki0%7Oa!Dcp(=)@fxDguQegT;6M(c zGdEUX-JDeRl5TZoV_DAI8oOnYDRms#X_^__yxC{iw7gV*Z?s6dH`LYF`eNVD!gS z6XT?%CWg5TffY2cd*h5jWdzs3_h zLls@F2b52nXSi8cej9K`;@1FbJ{~=9)Q~>V*U}70ou@ArdGEx^ztJEn0wL&Kub!N& zgNbZSQ6*0ATMeGKh7u43z%t&n8lHfNsn_t#CniioH#jmh4DW^fgSIlS9O4V#z;8&X z8!3Bv<3ihy2a&S;`pz7+q^x?;2 z??Tf1A6aNP(5u4Q;t^+Lt}u-tYY!)+MUt^}n{PXM^DuRuW$^A(tf=AiVo>TEB2rB( zPf5*OJr2Rbcb4MPsS;e&KalPEyJN^jN~IEFVor~-42~28gPJcRqoc#4SUyD9is)QLDsr7|6&OxtRZPm)Jzk$&V!y{m{$wjwddU>K|U{4=bMJ zw9{(AcB2lCoA8*}*m#4_`1tq*z46OpO$-4R5Gu}(LND6g_rJ(!KmUhh_jfUbKNw>& z$lblqg=f@jz3HKTYXXVw+y0r|hr;^jb}tZW44NtsG?2s?77^BbvzZdTQ3wjmYx4vg z%Goe7x88Z54hnCNLNhv>WitFT6k%-WjB8d%^o=5XNt?h@1>i^}uz0(>(4EwUkdRpb zFi*IM|F-n&SAoC@f9cRb4Qw8cUeuV4?pm0OXBtu>U7n9{}?|uTY^PzXndc)w9p^w0km9sIvL59t4Am+k+~>++KIgq#%T z7Y-u=>rMk_Pb0qk%eeO;oQ{pCNe`?k6T@*fcNUj|+}K9Q|KeL)1AGoFc$PF)R*sof zO)Sf%WMI&M2=-qaKo+_o4$0s3&Yt$xT}%k(&rZfy=m<-Q_RV|FD~rJ9H~#Naf>15F7Gbd#VG;7~L(yADTckCq$_Xkl)M}8DhI}4`uQXIj(*DIt zKZ0kO{!xUh0pY^Bii)y8*vhoO^u+ias3`g2>*!(j$(W!2m6)8-Ka~uxlynq~L8=Ah zh&i{gmkWm|4EYZ1J4ydYF^$rnfRg;rTcjyn5H*=lvVi%@$f5PiMD@R#X^ifqgf&wG zK?lL^(sEGL1tv*Jxs^!&RU~7yzjr&>Ai{qWzf3oovV>CZE&k7=@xn&=!}-Rse|3U9 z>t8eSpE{NQ`zrK*M;89SHTLcE5k5Xs7Eo4N+CW;uJRf471!a_#XY*x}&`J1qAGY58 zqqSE6X$_1{ym39NsWKHUteM?i>}(zS#LC_Zj2=l=0QK|3xg>bUVCVY01#khbGiQ^} z;2VpNn3gV_g%AOxWo3;*fzX0_dhE*7g#)Jw$hG%coVLcpd~ODvP`xj4+8thSD!4fK zL-EO>p`kxTCCh5H04Mw9flY4vuJ%u}_}Vbf@gFSwDD#{M?P9L)8&`ROh!~Y^u~5ji z-@Y9}j7bXX=&Hs|Fd9B&lOe}LTFoclF@`b9&!4BKr!)CIsXn5?-R)i}S*dGkvc9<{ zeK`1oNEJ4p(Q%GGM)ab>|)7DHIZ?erGbbO6{JI2}LP zAt0bu@s*kKoi{J1?I{p~Z5Moj7F8hDYX4yeI6&!3qh>KHtx+8|VsR|h&JXcpxUKW71~!^niAQ3OZ# z-Y;`lRzGY9$!NcnqN#0F*Rk6QdM}SwRaI3R_EY#LC2_UlHS>7vE(Y3Tt!TNVvv|+1 z@w%p9-aOuzrgtwr;ym8~g+&#J1f5@(op+lplSl@lGle#LLZbU4NLI#su^(v%6X=rB zTF)NGk(-GpZ9gDc9Bmt~nz2uL%4EEc>%UlS7j15Hh>2=(f2Eqeh}2q`$P|nO;5gH> z8l$MgR(#d&1>)D5Farpim{iNvXYw5%boo7Pm)bZk@&a;5f?Pb0caLrk7;lf(Y*!cG zkJ`ezghZxsEq+O!c%l5t^EC(Ridm!CfCH)4^g$N^ZPC=&*dBwGmDPSHnvkulyNn;) z($a)PLb7=QG)d4|U|d7EC3ZSLeQZ10X!o=|FLQSbMJ4$Pi_T@!RuoG3wlrh9=)6D5 zeS25!2XK2L=VOaY!s5H)(R9((HynxT#gIisU#WLF`8}Jt=9$t>Lu1&-EB%7Wg5hQVPWdLi(1$kH{^$&5^PKvx^h}H8 z4JR9}n~-9d#T!P=rjK`DE%&Xo7ydxM@q1lm) zc7Gq@@o*e+hI+yk92n>@P~&PKoHQD0r4!UJ+V;7Z`p=6!SwqZRBq8Vfj#hEslJu`W znCpFRbHTyE+i;DZ3nVrdubvM}MDi(ocioTSR-4!-<_PG^hVK}lBLz2%1ifBD)!0($ z83AwWuY)9w^xU-gg`L$+>}^j!25H&{RA%hp*Yy|#1?LwP4BCuL>j{^+A1`lLz`+lZ}Jtt>&$D8P~V(VB;op|p^R z1pIZUGr=T|owM`n(_Lv$5O;;;KvahGUV_WTYCvqUM(+L@uWNyPhW*j?W7{}Vm($`I zG9Duj(CK`!y1lfNR~DYOxcG#tjKRsaW77#@$oK&FA}Zp>puwLB^lBzOQ!<9Fxf!ia zCF<4N^(S22BHi#Pr^KCyunE8G>qq!eJe7E#9=W#i)erhclIpIXO9->z7r` zDk>_F6ru#*i%UK9|2Kd?5ZgW9Md(9X6-7#0ufW+n96}R^j5u}KtyG)zg zK2m`6xo99D?$yjQxM5OX}m$3772hN40Xzv(Y2AGuaiF1XxpPnC$hK&O;*i_iHXg)8xByN=#Qd^DETfJH#mcBJo2Sm ztXJxV)0*wT(?u+~bgE@`7wfOLx3?8Kt)Vo~tIuw#fg!;mK_POPd`G7#j#5KXMFU7U zbX6F=R)!F?zN?Ge@@)S3@o5WQtOntpn5MFf&u$WmvwL4I?dLC1orQ9>gXQ{@-|8z3 zw&vEWsaq$J_-y;V0|O~(DfCHd{&CGMy3Sp__T&x7xcD4w$19Z=ug~b0MJTB6>oa*$ zFx+=~I%nHGx$yARlHscT29d7YB+%FpG8@OV*e}fIc81~#9S=rVTAUr}cT}}4cwJ9L z2>b8mOVy$=euYNhT+pzvU9LaBstj0W@VHtn*G;{8qLbf6lkkp4ng03}^?3RQN6@6W zCr#t!_86Gik7A4VjYnjXGcZZao*tmlY3ehxp@0@?($ zr4wboKg9g@E#C97yIH9)>Kf_VJ?{1XY^8~1$GOfK{*~*)h0RANk)H0vFFK>=eIsi} zF-n#@s^1QVW=zdwURuVhw|~9uGt?o$O3BEG@^)EHwY7^&E3z_Tc{74;CHG}JFf#zpBAqs4#D015s{CaHy)aIx9FEyoW}6%{n-H_ z{R%`I)iRQZ`qYKgY^D=NSYDt_46w`DGCtsEQSei_TIG2^17Si!0{SVWWcHBdeAdpi zT-P`xQsDJzG@YAx*u`(G8y!X_JBK2)5%1{}y((j=O6z<80{WC=H}70t`TLr&coN1+ z9Z20tmrIHML4#Ji%UW=~8|qGBK|z04KnR(5iAtra8P5l+s0AYRrT4j@+>q!=&wak? zoChTPhx1oZ(^v0z-XAH_Gct%Pbbt038thCW_*VjCX?ba21@vEQMxFgLF^H+0xue9vHQ zEy3q6D)Q&WU1@wcK}nSn0DcL&ybRr=PsH^g8~^IDt1ib7xj&Zr{PdX9Xk^Xrz8IRV zooW_V<8a6{#HiJhBU*?illTQQL*GP#D#~AbYNoWqVlQ=Ehexe4n?8!o4P?O_$VvlXC%ZsvkYP_UT)q)5)5UbdvW zyGIr;px{sTwbZX)X6T3XqN2KvZBLK207#u^>B%0u;Vs67XZyu!y`i+ja&Tl?TU-0V zSXQ{*8WIjUC|jXv>vf@0Hw3d*WANrdB)!TSMNt(}r27pDZ?+!>=EYu56p9z<<$ZE* z&1XmEd$HcE24sGJ@OZxlu@(q#T!&XX)L7K==WDGCG6!l9E1+BN0oPM`^{JkPeMHIB zO_%DVcwUFc+1aUSco@Y^V=7jyQe!9;o=P^I*#T69O310x)_daBQ-n=`>2SM9zL;Fq zXbTj$q-W)$?UrGqV|D1ePVj!ZH+kK{qLx#ycesNXo-&Vz-3Zmkbr45qAVE{oMw>gY zP4`z$s&vC9{6g9K;K73fM^^2whK6vvXL{PbQOVHgNTAOR!OCYbGWq^6SLJMc-*C6* z`|>g~QWlHdKj46vL2$tHV!Pfuph)dq)o~X#9N!nL zZMiYv0C&nK=8|Z{5dVp<6%RhjpHaiP^d)u`(m1gNZYu*NFY3c1kUAgu{v&@H9y|)f#iCj;paPqDUD-@ zWq5wJKHLOXV=>A#5x{x2ynTTO0N76pqj8U!+jP}Z!6G31YE}!q&{*<(xJY4hy)PK! zD{U?;#3PI%84Zp+fw)yfjJ&6d)vr%urI&{6*29iaB}FCrcwkf_npJoI%Ii*WKma_t zrgbAy`*B8V1BLLRg-c~Xk4gxARF;5`{5RQ7_-BQ5=RBosA*u*sULRR8vEIPxtA zo)5AqChhJcRH2`mvSBIM%@#jY{4yR%LLudT*?JAJ!)7*^EVslv!~z*@76^&XP0l(! zviM(t+Y$IZ@5KC87ey_w1{JUx9Rg4Q@kikYOo^4+NYK8c3Rx^?5dXQ_%eXYJY_`Su1Oc7o z|HIl_#Z~!5U%x6M4T5wkh;(Z ze2!Oofjk@5v(}tre#cnWyKR|f?K*U53tz%5euxbvA*9HDCh(oU$M4gaJlx2YJ3;ok zIr+7Ql!4E78Q(EFHYS|vcD~deF ztfKtx_3CG>hO_A?k;3hmsifN2Lprrfq;!~lD6ZOzWf#}eBOjX|vuTv%G&D4r-HlGE znuMoT?kYGq@mRCm-)|a>`zNWu7^{W9MU#-VUU;fB9*<^O-rw-W$P;$4df9^W85$w) z=)gdzMg{6fiLCp%@vooP7$e>e`!mzU8sheYjZR0F%T)%8H|5TH2mu5AeeoLG5WQJ_ z2YY)TU*Xxbqa!O?I=Z@-WY)to#Wd!LCgWKMJHqC36{3}fV#HR55L{LVFkDeVnN9<}Ro+(ppLN6mP zzffq@6M7T;U}9=&S~(hU`K7VZ&Dhig6?Zmm$zO$6T30v_XLpFE0g+RitMzC!gRjsN z@3AYva44z1zLDwqxKGPLcKP{|m)&fpMCAG*yfX`xq$l*ik!|t_rtv zw?&O&z0NmZ`xcQY`%D6?hFjgKdY|zA9kV(&UmIE+W{dhGaxd2S`DImwA#;>TiUn|d zWv5Xrf(_iCE*!Af-#Z(G%g;CYeuUMLm4eVHU+8GTrZfdwr~R3cDGD>w$t=ChH(}0g zq^Qk-^!R2rqxtDDZUPqw`yagNzdM_8TDV+6VfTaHlHl>r3T8;0bBy2MT%L8)7{4ho zlnIj=ew~UXpw_42m|1O8XrWQ-)^e^d+5-O-|0d9KYesOnPUqsgmX@7^tF*LjgL1J( zwGY{r?~1r(RSkUBEy@cNpCv(#M7qwClarA&0lAvgE-|yIhV1BXkJoHGc`q$)h7Z$N zX0x-iU~X?|;cX%c6tWS?-q@bmzJ>7#?RUR}BH)at>%m;9C!S9g)y8q$Wg=T?y^wNV z&~@}qyK266Gq+xHLJuG|tN!`(g=|_GtI50{%NLK6 zOW(R`jmn6H`;;}Vx)#Kub^6Ef$Uuv5d{-Z3VNffvCu@*3NKUC04efg*jp}uwK)7Y8%tV3yX5-akPIOOrCHnzkVIS zeC<6DSDL4li=DNr-sm8-(z2#D#P5a#+anb15>p@$Zh(T%`qo{$og6huB~9ByPtk0u z04qfOL+i^Cm-dBLTde!~%AK`Ku>a8DitDQNaqR|1vVjLNa#`)sJ3vDs*3RN6g;48y z=U)uOF*rBDV>_!Ft6G0$G|iHxOZ)-I|Pg5bSFx(Scd(d(Dc*B!07|9p2NWdY8%{++J;Z`IF4bnxZ$Q=$A@N^4AcS7Ik}4ip8#@!Leb z{>}V)n27|PH^Y=P_mzp8-TSgO)=LaEffTaJpdK(BNxa{kptu=5VIgPBCRC(5! zF*Gt_AqU?vj$Dnar12A3p2q87xz!~hg>f#7i0-`lUl9>`9~!%PsCBCBtNdcWiEckf zTMXK|c#nHx+Ah>s*o?HF-mgTp1D*CWN3(opM;sOQSkFa)N@z%kj~ovx>uc?FMG86p z0KfI|_rmcQUpv@)&`HwX%aLpvSURwUaogTNAWWIKOuB`t#`wj*|DL?Mk(8`|^8!pl!MkP~iWOR?k6s)~ zh4{tp=0p#ajXosKC#yoWR!dy=Cnt&3h2322TwHi~Z{6Wf24h}b1MB)04NfEp&uQO3 zgk6#f0rN_0;>CLZoUp*XIXfdlBEnLwWFu**@|6)gef&p=HNy+LlxAwgaus zo#d+qsCuTXX3LoP$d*p$v8G|YOJ>(ZK<16a%LI1T&1yTv3CD-KYlE&FXYpW!L4`V( zQY|}7tb!mOt=8XvbDe8o!%zKJ!Hi{m1tE>*H>` zC(X|xE(72h?LBwFaoz9U+VrZvqlCO?ZTrej-#f$mC9&B?Ph`G)<{QQDrc)(bm%~zw zewUi%k^Q9dO<^(pTh4WjNRP;?tqi7nJ89`3SI`nD%j{2TtvU*iMiN;nfk+|}7@=Ba z{4ZFscWP=f{uN}hexhG#U}r?5%xlHFM+P5<+uUz$!Aox)p^xuhH{CTkBUEpkuh`XA z;1pxBFu#`#7wj<&4p_l!WC6`y$Cc*06`8#ev>N5nJ5GFRBpfCgaC-!kU|wycdt-UY}#!8hgHSc&&SP@q*KAyj&`; z0i0-MdZqu5U#p-=O8>Ja&u>RP1K%_6;PR)!QoFTVHb*!ai$>XEu`ECR7~?^p9R)Ko zI6OQkXmp0by(|x&dhdjg6I7uJ3d-M~U;_g_lH;^f_(IznK6pYC@7U1Ol5Tdix=a={ zM}kwdau|A4=SI>VlGs?*->iB-Z+=lBLf*HyKPn6cw+lA8iA%1#(-m<0Vmb!~tc#(pr`jxkNip~A6v zNyLrG|Cm5k6u!H=%f;uaS((G{)p(aFF3Wb|w|~J=%M<}R*v#W2(i0neFzLZz(EM(c zN`Q;c>k8?TjWWFXK?{1df$pMiFEIpE#p~f@;dkuOCgtL^Wpk<87Bk=h9+?WzfsAi zynG;*_MIN#H4PjimN8ak4yif1<>_{dl@?aVbtg>^1Pou z1VgrJ)205Akz=z}mA){EKVF`$Rf@T?MkC2T1k~n4b-my1Q~lW)Om*A13un;bFU-$x zY;hE7{m9B{RE&RNf=!#;YPy9_xo5XCY-FH6&6*c1D?r#`?uSkmAm9dF92#mia-vbJ zbcEbxhH5s-7Mrl-Fs+095g9J;OLc&a)yeV2*etiWxb1ISs2+_Zb5qcf8+4?-OG-{Y z>+zjgl8}=MG=p@X;~-nTO%x`3Si9JhbSp!!E;o|ta)=hLDB#|c6hAwTGK_vVKRNf#M> z(h&|9%gt6&JwU50f^#^qquF6N(KCdO{>yBpm@=5$O$MJ@S7vV(KHOkQN##+&}r zJn{Jt&x1mh(yf~xis(Y}@?+!U(8LWk7R3qEsX|acuWMP#sb|n?G<59#nn-Vlnf2F0 zf_;-&b2!hXX#>u-hLD227%5cJo*6n-Mz1v=QG#i!gYjx9TiY}LN*wJbXFU@W`a8^S zo)8$1qp%D6FL7NrpG(w}*sLPl*)7H~FY64>Yrqc@uvSMaUW^|(FJ^57suk3jD?$50LSRUAV=^DbPMkjp32#0ml8eCDDcjVHLir;JP zSxCDVxn4He-Cre6ndM5SXcn8sIHhW9)S8>k)X+7CjBoyX*AtPT%VO2O`oTA2Ka!BE zpriynIyuuZ9UEx+UUwIGSXePYYxjJ(vv_Lp#Y@2ReS@MOA}d!&4Ak_`iiAwIb>80X z=Q|^EX}m0nPg8mB8$8^c%S|qBb7$nqOM#ircr-Bs`QPf#nz+~%bJe6Iq;y)G@hl9r z+E?5+B0j4P6G?VS1A}4lO>XvEXO@&C?&f*)C?>kApDF zX0TcnkZGlFn3!18R;rEqrwU&nHt%};{Q^GJmL*w>)SY{5Wk60a1kAm@Cy zqtQs}MhhJrh3VH(j@eQcW)?l96Zf56>+W)!4KZ`SY3P^+MMRqMLJb{HjZ{=o5v}T0 zMXQ(9%=GKxt*8g|x&t#hNC*Fhz(BB&HUxz)K1 zrm$FTLVh`O@w=WtAY(tg#>58zzw#JK75Yw|mPb|aMP~=~e;{$)z|f%O=_iiG?szwiN=ZdR>szc4)Cu7Q*u_9?qv7PCp~mg;R_jy3+K#e{zrUMa zZI(49&z?_@`URknAl|yJcS&#>@;Dw@ug^+Y9yhz8B|>kk?OOyFiZsi)IXQ9FjboMy zlBHTaa8l^#QMro_dV(i<=OWxxRd1SZ+0_yfzJ70ZyXJL=P?E79EUMEDkq(v-vAg#z za-ADVr@^94CA(BwEj4swI&;M${lOSX<@b7Agtg}wBhhO3PIZr>fDX>Li0&GxRoCye z^`^~EPZ3p;E>$5YN$lVWOu>A%f|(f%eQ346)6`L-9!&#be5u|8E`~CGJ3~Z#?R?F% z;$LE7g5_+V2D`*(f{DTmr>ziQPnqS;i~wzvcxuzhLAFmdlMC@Tt3?F0=9-PBhlxs# zQGCX>+lGec=C;^lOhrm&-343sR|jc4Zp0;piaQZ0+>QW#AkQr@97;vdvf0}-;kEc` zDU--vlWM932{b*24J4y5Q!bus@qjCmckV$Y;@0ozI)&}_y6lIo&}sa6EP%!EgS<7F zK845704#AfoR+V+UkyhRx;(`vZfR&)+&)Q;_))DiF)YPK-Jp%BB*kQ2XR zyxqe3;WJsN;?)1m?8}hw6mw_kdQ4&5@ltb|hoLY{1TNS2;w5gOwXo}9uqdos8KM3N zGbNSppo!7-{&@Y=D^l1?4IwXy2gD<`IJM&FHE649(^XOr|H3wnX^2N2&Nbt>{Pfnn zt%ylB(e20Fhd|H5^BX}!+t z*fZdXncG^Ip>h6I(H;SNu}0;*Wy+Fklg?LTDIhO!dj3{Dr#cg{$FgpF(O`1kqs8Mi z6weMb?2d_}x%Dg=KVudGGWFw$Yr8j}${mI}1cK$&+j{fz^Ak#Q%^*?eV0Gi}@Q@=| zQU>|Cs~|{9No`EW2Zux)ja5@v!K)qNSOC;o->y3ZEA-(ADis_YE;>`07n&3Df?wYm zRtcnWy6S=?n5<}hdAr|u!X+qXEg-WC4@x;|B8*K-ct*4isnrB2C1@GrFDpZX{*ZD zy)C&@S>7JrP8&Kqa}r9DVxJ)(pbFVx+39JIrEd`9q07tH{)HHK21FEPsCeA=8~n|D zUKCA)yt8^>@_K49+cKPI|-cA&;-d;I`V^*$d!_Sw9QxEOJY7~|R6&w;@6RyW6|8;uU!IS1K z=`ruGI06;%zHU-mls1%?Z+fD`*_C;9Gu{zGNk0okC`Sli7TgHJVJG6JHotuN2*hAr z#OD)kZL6(FEi2Gb;%IUF?lu{urS1J`KQyIq<^cndFO?Q;Q zw?$*DYfL+DNNjqgxjo0XQbs?eRo{3l-w&qM{#=WU);4ZT|6)l32Iyc~!o_E+-aL-_ zG%IMF3X5cc3({3fy>-l{i_}JqlA|$WfWY`M3*v_L`U7GeO(ipnMz#5WXcNHTTpp)C zjJhD4pw-!3r$*GoN-VJBLyt?bV@5|uIe=K%;sGLk0f=mv zFYf^nz?9vUtY&AmQh(ll+*zha1~4LApFSCL^PP`lY5pb9bbEMp|VKzQsMgsZr^alm4PJ<5>6Zq{3 zp_705#GF0z=4@*?rIEvGnYqZI59<1wq2TK)m{;hx!-qVBX<~WM&W;X5({gz6fJh>~ z7Q1_TaX9g`YkttYxnz=4kat3U7VaC*aUCxg6&1bofE!9>3#9sKrPGQ+!u~37VNgf) zYckkq;VuX!mn$;;6SlGt{zxrRJ|ltALdNi{^8K%G?i=gtz6{U?vJm_t869$9CNyBX=jV8kWkPBJlEkxqM6>H;i*=e#<*VYGd(dF zo0&zXF~+VfSb`5w!pYg_SNWfzL_F{rKr%Oc z%+sp32$*xnGubuh66W0vCDkm}J5cbEW!lE{gAgb8tAqO1RxcNWE!a@(_=E&oCOvrV zGF^FTY2<$1Pu!o5cL@mzVOrmmER}s)8C-0(a~j&K=H}*ph8AD8>F}BL9OTF(smi5b zk`P!~c^Mx-PJ3mEl~fe}TSZSgUtT=DbANhl)~vT(903auAy{V%8W|MUu>zzH7Ey7^ zG#^@x`h&;jduw>rs@kYuv4)-uq|D0H2#@3Vx0TQun&S4rTYR=X9b_;fptA7PB7jFgc(^&;Xu7pKKkCaO{CKfqx{r;!ClDi^CeY;2?0+S=aCmC5@G!31hL)Ny~`w9e5@ zU)vc>P?>aZGc~23s3YQRIrt{#N+S zDCz}0kr!6LSi_FRt~46?cBhvxJlH>9=~gc-8@O`DBg^M#s?@LmEsxU4&SEuzn%@Er3ds)3(Cv*!$pk z4otwE!MO;aiNHu)^=#9aCZn*>Z~}hw&+7PA9~XoPH1P0tO(t^HYihgXtd6ha=rui0 z*ZmU{6U!y`fnQ!(Ny#Ac&T4e@h_3`c=?{-hRB$(N$=oh`075}TL_7&F zG0p?s_WntE=k5HiyilX#!PV8pbV<7JC)S(Rc4J{s!v?~H>(H99TSPC_nKXNU7`eAU z?I7nwwEhAJH5}SxhHvu_NP4XN_taEv4l5mTI&tw{eIp~aONhbbpz*))Ze%aUojI1B zR9;Vf9D;>9lRvvGIeUA1+Ztqb<0VTC1^M}e{O+t&RJX0|z5(I5cPDF^?|S8KTCR(= zYKJQfq*gK3);F?L=(%{^9yOK4<|ha3JGu}6j<>jsw8KEULn7cIZ1#B=?^lqlE%jr@t9r~OgB!=;r3Y2t!Btf;32BEkKCOUKVnE|NbK^Tw^4WPf=9Kf5HEIy~BP)U}IoLhR}lJ;TzOF zohV}}NDcLy!};o%m|v&vJKYGYjZVi)^)9_bOEMZ7a7q)oR#xl;X#!rUTprVVY=t(< z`!kkpfw9*ZpKD3RVl*Hq1JJw(MI+e~o!YV6ZYdcH3!vzXVkKW9?y5SL;j$RAo^NO; zG1@=@#OUGS@!_)3{&2BaLxsNjd(}%D#EkS;gP}>GG>_~ii9cq3yz5_vI}i%eWTwcN%g=Wu>CgVkd1V$RO6Qf^f^ zbaG?{r)3muBf}{G@eRd!9ZjGRDi>sBG+(k3$$>5|G@{X0V~c@7z+UnrQ)rin*I2;e zZt@D6%Ze-DD7;mm6ici2QFb?&(#y%|8lcYUvP31E{O%{#U47`38lP3mzdIOz>ANCY z@GFoL6Ej?_V?x=R5iehTIpt?1a5$dLQd3T|Vwb}0fWO7)xjyT|%wlzvY{}Qa1nWCf zRT#QowL4K~TA$RrF4*yIyYSOjlsYjWM=;L zZSmmoxI6ewKJdQnOe}yj5wM-hMF^(|Y5eYqOddIrkqoFt6f)%)D%RB)J8`8@7^tG6 z!v0KYTw)@((^b}O@Rx76?- zR8(rKCaa9((lRm~byr@p^B+6}MS86yBo`SP7*>3kjR=mA`0^}&hsk}BmXVV3UgIX; znx8T&le3#^wB0=ebA!>HHlC@WsY8ubj)1kxF2kYtbGB6tcTGY96bV>Ql&S=)q?i~3 zJw1dNOBSy+IDuC8fj>B0??)|%8q}+i#|T7B2>G3z+j$ob4vt~oT!<56&(nTps`xdg z2ZogqX)ADr+I*LnDBHsc*N6KmjyPvyO$O&b#3O;k9;qD~7l(6zbW1NnPDZoP>{bhg zvU-ix5@zNqYoW=Y$jy~a2w09ca61QtUR%TqDS^AU2l8)k0mZ-4U|=TGjE{XAeB0&M zmB3=O()g6_us;nzNiN5O*=#8l=j@c27!Kw;){UMpGz3(GC4Gau=!id6AS%;k;_8i!AAkRAyDYLZ7KwOFOXu5V> zNy_5VA`m(JdF|@wn@rAIn(x6e=XP=McssBo@vc~h*XtL@`PX@4tg^|nh5rSHwqAR# zl>fLNERtR8j|Ga`$_3IyN$vKWydRZr4W(^s3SE$&zYDq$KU{sj6&6K@hyT!z{ff^l(8Xh=h%8%J&|v1GiUHkbCj9{Gypo%Z&(hwdY`_ zmBjNeU;3@huh6kKh;*;fVdHT)oY1hau!iUQK6wHi1eet9ayKS6wZ^u45A6GE_CQ4$ z3JW`0tZ(#u5IZ`Fp((%4vdj)?E&?2Hb8r34K6+M=VwRAQ&_wQ#f|BAEG9@smaJ#Hy z(zYt|xw*DnE>_uu>bj$kXf)%yI=k4&9p{duk`XYZw(5HY`1`v!H!+RQ;N)JM`O`dw zG+>=Z`IQ44d2cpP?T?D(+weTLh3=Z7J5irXqoD-eF?_k0rN*l^!RZ1AsV}EPNwzto zPLSD*_m>1%L>|yt8+bQ5I=bjcP>(J;^FHdt=E@IllmK9ZN!0lTBFpI--F0ZJ_UH8~KQ#{hfjmO@& z)nR~LKtRA=;q;SOU#EDa^I8fwxQvFVMj%Y9fVyD8(L7%(_X?z^S!{1r3im8b?b z4jP@k_GUPy@^W)O&soF=P^0fxQAY8h=>|1_lm*`{Cfs z5X@Lz9L!~&qEVz4ACltX&9NBCl@Gfc?Wiag=L!+EZ;ptaoL%rcJt8}Ce{ejS4R+;q z?;?)zPlHrk?++00CZzfNSQV9%kezFC_yMdcCzURth776AKmTOOgJEDg>K5(D*g}S ziI&}eayeTNV>W7VFkg)%`D0=F5EP|mGh2Dg_ALV3o`WhAA+nN;_^u7T5kzTLERh6? z*8AIZHzu`*v;7TXm!^B7Pw~Mj5dToGY~&ICEr3cZS4o&h;$Nn z%JR6sUe6!+gantLoh{ljB37P`@k_Qw24I!Gr4{o zv7&PB#Qv7$EH0hIoeA2pBn}IGqL^CC<$x`x;l;TF+%xpdfJj1qbw)O@g;XtMibph` zo)}v!;-Bi7cUKZAYh4L0R^FHmwxWo}5i8<0{Th8|+UB>Et|xM2;&Ob%rzs59INswZ z0WKR*wfg(}0O-$8Mg}v4JQ6763l~nrf_kScqNVlujxne2Umq#~Kjy+h&CySNgX1ky zbi8hj;jc1$GmNCFrnt*_C0}N_{nvYYdIC=4`Xs+de|HWDeyc-562PG(Goi085ux({o$Yh9ua(2=NHR9&MTQ{-ccEy0qmxW zZz+UV%I$V!;a$4XaNsxt*UXsNEOLvBQNq2?Nk2l&-r~IUkRc+n(05pFn&V_6FrLe3 z{>lyX_U%}%yVrLl0k^B^Q9iOEC90JqWJ9XGw6$G9zvLC|`yX1}^=@bgM0PU>y1C+3 zyWZlXa@)^(jfzUe(RN>rZ-wfi{zURYhp~@;KQA7R-|mp~9!tH(bhrQ5yc{>F3VSTU z(aFnKmZi}1Wdpv$K6YIniYp6b)&m+}dB;jUW0--$(INA;u0no(diq^`R8uxenp|#4 zN`jBe78HLPTbbg8CM@ru<&h<9w>Vt%lppfQo3U3iGqZJU_mwg{skBcWP6ll(0m1~_ z_V6@nLb<~!*6EEjRxV1co}2D~A=In@UKLDxu`vl7pe^7m*MlDM3JX^@Kc|2ghk$^X zxc!lk#qgpGU8K9onyEBeR81{kv=IA%{Rrai3-{Mim0}PSkk3vAYLfKy_bb_EMi)+G zq@?j3t4CzX&Edfk*4AY=Zfn8c?;qM0eVk^>(0Tdz^yCscW=y!cnH^az2~{57stI+~ z7k_?-hhA?mferQT90k3vYP$7Cm2o>w9vB|xQ{^WkNC*pe^!FncuP^b){fcXITg9CB z?8;kAPG1*8>ZLS77M(MM8 zkNg?cNH!@UH5OHaj^pZPD2d}s@zYPw13BKs`6{M0RP=`?i_!F!-@nJraF(xn(MZr9_)w5d*Rj9IN-kH~upHDlngQm#@$Mqw zPqIOVn9MT%4;_9l^k1mvMk@3?VR#$?ZXKABb*jrRUSkI7zQ1Srz@OG#n8wM6$8P`e zvE4B&ji=^V*9**Vqy8J)Q}-vs)djZ|V1y^J26RAIujMIG8~7o3C!`$zg2U(yi`4^` zmyhc?8ChCgFYy@+gH$;4@IheF`r>JW>v zk}j!wezS@dGkk(zYe6*4YGJR^V8W%HNpjg}*=o9O5J$-dX;AP@@bs^U1PwQK!!o-Rs-iHfvDW3y+Mg z*O7-)y?gDxa2~FgqQ^~2h003}PCtxLL+#eSYf~W|!M8(aiZTN2L1Tt;ak*wICoPG= zD^Kec=K9QCqKbViduy{$$DP3=da>M4e|3iMLjA;&(Q@ozcOM8G&8BUB&E4YYzy?I@*dgSbNb4PnC--AD z=`}YkkEEoSA(s1Y7z}?f)W0})8D6$DFW-a}d+VsqFNNZXK*p>YO>P1Mi|vC8O9@Xa zM2$j#2)hT*ZYv=nL`6c2Y#HjgynZ{~Bx*N3fmE-NZ)>nPPwB$J0WOYcAZ?P-c-IbtM9nuA zqx}|GhYbx6%e<3d6L#B4dS6qkRUyzGx zdy$fhIyaa|N-}rGS)aW^x`m&!S^J~~oKt~~a9PKlaA*l6M>+wu@3D`u=sD}Zh_R;y zixqv>m-w=JskVjXxgCC)OMa#k>#ray{CDyCSkni96fWY0)YLTO6ci#9=@k8R5wQ7D zR?Ct&$#H8g7`7>2B-GUMa-Um@8&a`IJlI-f-aV~G2fA+$%+Mp)N9t{DTZHN`R1a{Y zoRDG@=eWYe>IshRS_KN3PWtF@D$fRF)%kFb&F#LkH-b122{mG4^#nF{#9E>L-vG+|bMm$|HSMk|~}I zC2n8KPt^E|&|Hj2N=+>WsH-KnD`8igmMy5lJ=drD(VgOQ37W>Keem1X!?85f_hSyc z{nvSjvN)rsr`OZd6NVd9a^dquQnIL@gEQbYTwyc=!%d&US7^donEhsDTcUqHKEnmD zs9|8(9dT-^C@U-5ONS*FX$qO$Z-wT3vO+Pvxq>}J8j~NpDa^?D#nBSbz|F(R6D#19 zuU=h%Lcpf`JjH2^Vy5`HQs-uWoAHg0*Ey5bY;OiLL{Uo)t<(bY*U4(R{QS+>54ZcH z6kWm##;}F>RhixO^$z;D@oc-{AaM~95fvO25T^~`O7kzvYoWrE1zJcpt0CrR8~gk( z-Nj$>(5svUE--%l_&KfBrw=HGuxKOebTc10D_NsBUUS#J@!UAg0l4?aM_aYgL-X{~ zQj!LqK?bq3)^+rfJmte=y`H8s_j_fjbh-13e=KhIA`>}sg^G<=Cu?M8lcPyiEGo%e zb8;S}Hl6C#b!~!Af;XdxO0CEv7&^qVsXrIZlI!XlLpfi2T>OVI5~BVej4?(z^)aDE zNT#N$Ud{%yx(W=VN9}wlhAaq}Z!>VX)w-I?Ki;Iex8gFkxn``BSU|xbf zb3ETJH>NOlB&}BWsRag{V%X3cxDuOwRx_yY)Q?|7>skxff-mCk{M|N7zFut?f%pso@6$=;|l=1%_Vy6*|K>LbJEMgW26hle1v#{vM zD9BOsb*Dc0LFO(W8v1oyXW$Nx|6HH5~>|ER4qaXHOsI?N30X4EEaOf{MnHvm|I9q45yw0pgTIBcV zxB6h?QaTI>T-T-qmbBU&)-WBw9=dKkzM*%`^m!=pd0vjA#d!q80LxumQp!*{E!#R+ zT3h>dAVy7+;ZpC@4|&Y{0OvL*H0D!g2QATQ zMJFT#E&`9EN}B5Z6W2G_M|?NHRrBjvqSLZGO^$1TwvXh>Cd=;78FeQu?X;03s+Q6p zcOxwD?pHSKS!1c=Xkj~+zW4cl-H+AVlq;APz_zK=WPb}ZDD(9Fec|Z(x8(xLQ`8ug zcsJujL?yN#v{RR~=it|DQ!KdHzb_=ABlQ&Rg*yoDLso(-<_h-1)N z6Yl8jl-H@=pEm63Y#VLc9s6-K4m$r8Oy2O5_6N8?&oDZ_&%0xpJjf^qc~Vr5y4x3F z7s>i4&V1c8uh__CO{D&P$vqG3sAyw8yZEOlH0KT?J18Fg&VRp{;f{Vf)NrbMYmfMh znRb%cjx3}5a5>r7P-!dB?m>^X*Sl-J@`sw^h@bOmLC8r29M6tYlD#M1BKKIR!+E0U zGt}5K@P7P=X%V&*KwA&AazdS=xFT>GeNCc&^@%ze_QByw7Wz1SqA}i>nVNx_zP0|MtT&Fb)8c5^OOG1-=K2Z`)5nj+wMSd0JZCu4SETs=1aa z`4c%pPb6IKAk1Tw<0n!3(!&nnpG5q}H(UKdG$LSK!V=JuYDqu^NZl?-+ zWRfud`KnWnD=3(}k{c*cDtHZh76#8|y7%wj03dBrQCc0&*|o2kpuG{1XjarhOWY2K zk`q}L1WB;xuunpJ8-L5ZwUD3MW1Khx-Y?1oR`{&8Iv#S*OzLfmv^ySa_nnRX#`w)h zNf~YahWS}&f}^rCl(lDkgNE-@%P&1?V`Ed+t^T0utMc~$4IEwJi*6Mr3k(h}XZC`X z54aX>PeWRcz~h_0?JkTY5x6>>Aen#9Pv2rt7z@!0>YEBe?;IO?INOq1(SE>ZGld*L zL--2LBOUCA=mxEp>s;b)u5ES<`E)MeRwn5+J+fy`W3I*+RZu6Cxizg#xZ$t#P6wf_B!&KbH-*0j=TOcsZU z4uvZ74u{L+{QQ;!+lLJT0UTr$foMd86b1SX2 zW@f<4Rx5HPK00AKm}VbNA6jDBT^KR*O-adYrT{kc4dPze!v_fjgd0J35BF{=i+3B- zkls%8T%F1Q?~Ju*sy}NbX*@>DePd);n3%+T&i24z?Wi?}QK8!o^+&?CP;D)jnxO~x zdEg9duX@b*wLaoOv4H%<>wdy`rw2^;fzhI&80D;mL0PZm6_kNv(R4BX6d=zFH6Aog z7D7UlZ6bIH31vdgE$8-i>$6~VS`#FB-7%I7jA{woL-=AL12_yIFQDYUoV8Y|G}Xy+ zMZ41VFgpJaNU}T|o-o^wwK$xwIZWDF$ZdrS3VaE3Pr_x7>m)OPP1n@ioFSbEQUYcr zZgSpvXuKS8(%$Ha=sh1Y%#g|0@ozS{h?hTc)oOOFmP=bM6Ms(t&Pwljlj$O=#Jfii zI6zyzLBUrwR`9P$Qz+8Bxk5V-FdpyeS{S%ree?wvKI}&s<#S;xVNLP5T}eqwDi=vKZRg{&rrJ~}ZVm`m_U~Q>9d08dgYp%;H^#N4+AZ96kM=Dr_I{QP=$(N> z$y{#JgL5!&C_hD4!IT@g*%voAeFU$DQw4_p(`@JrWM9Z7^R6AGxXDH)XJuu*Cj+z8 zwByFI6q5x|FUclz!uC=%l};E++c`MA!)SvrCnjlda&wdBUlGyPTiag%1<(gIHngxX zIm{d&y@0Fv3(IKFjOy)YZRgNxw?nVlGKvA<-Yvtfp=35ba;n?YKLFf1gbAr0$~8JwU}|cTpAX>!fi(;|<@SK@ z^Y`5I6;)P#bNh?ldF&HmdEY50UR7$)_1-s?zWYc7y(%{Bb}zM*zh+gR@x_V z0!SSd&Dld8H!Qis+KsM6Twq?`K3z=E9XHB&QmWbPeth(BP(kyx7!(z%X0`My3cI7_ zoo*x~y)+RZZ_4P#cB{?1xkv*B$7f#{$LQ=ca0ta^BpEaszue^GzIJmw7EKX;#8RIJ z$|=NZu2>CF@h-b(uopxe7Rz-&4#b-GO2 zXaH=qR&w-jHNXsqCF8<@ebP)~Z&fh;6ug8r6HkpT{iYFF<&twn+yrhF5 zqyhkU^(Vo$mz{{>_N1+y$G|@$6K?Q|ZPBgU<=UhzxqXn?5 zW?Yo*dAkb1YlTPa=9O<0n<|J08@nQb*FMSABjb%Bfg@Ow3G4%ZhpJ5J}8uj>7=LWH`dYz<=sT z)!W}+AnYFk=-rsu*xkwOKQ*z_0DR*2x?L=Go57f`(Kpb)Ra`vRvDAX(m>DdZZsc4h zv0EevSiKBm0Jdw5a$B{QcRvAe9rQ%ek7XsTqD{E+v8IHoH~PiRkJdMy09JtMviL4c zswi6&BMb(ztIBkc4N}Nl+v8G+)&huUf)6kPe+&(5kf6Xbd^@;n=S;pqPIqR`3-?1+ z6_-O^27b0iK2?}+zP@tX-_JjV(*=_P1r@O*Poq42v0@@^HBEY?4;~H4_oW7u7$fc_ zllUi`E-)XgaJoPsEOd^Rc(i|DHFVJm;x4Tsx4Leame167x}ij(T8?>R zC>WcPyD!IL_vIp6{bJKTonIi~cD?2>c93wB+(X``}`F~Lzpcsk3O`w19U}L#RPJa z;nN5#_AQ2&^LtZ;YH|t4J8zO3W=fZIopt&x+A-fW7{EG=CM?oO?ZAeFCIu!l*t+9B zUJJigi9k>L?UWPhq|HRtb0*+}dH3&~&}a8QF!Y%1Y2B2EO(fW2BfB^bsdZC2=ROh6 zzijDAsnFAx5Ivd2gA|&qRU&?gEn$eAdW+;sAN z7Wn8TPw=+#1>mWjch|RiWx>@l%-ye!*-mvV$mNRtMi$0}8UJ(!prD|jCNX&^2cjMp zyJ`Sk3QNwx{wMU|K}&0K^`0)qVfgck^6e3pE7kKE@U#N;qIUqS8kf_R=GB6zNWjAk zILzCdhY~poze!wA70#3fJVDo`j+d1!yl4?+p1;WFTdB6keS`%*es?)3I_=vuz#rQm zk2wI&=zM!vPF6Bt4&<`Jc)sA{;{#NPXoW2X_^QvRlmOl88-2o9e|K?Fz(W3sj?Mz` z71lUaCO?v$hA^6Zoeq}Q4!0hzhBTgj{se{Uq}A8f1m`p_$~Rh#P7;#R-{F1wB7v$M zqKX%PBV{sK_vS;Xm4zHjOD{ieuk_2dx92prIgQdLyVY^O-)Mli= zZ?!zUqlqKJ{a+6aoE@zZ()!b!I=@4W2u`xTtAq%Uq45^)?o6y6&Jj(4_ z7N_Z^3=uKRDkl)p2IvJjIXNT5P5{ZM9By9JrNe`lx7K_)B>ev-J<)w9a);l~b#-;Q zUxORqL0}s8CaV8M3ZvzEhyhK)-?t<ghC{q6$c1Hf$$dF(k|FX}jBgo7-Xg`a~M zw~&1?|0k=}%coWDMW+tpZB|JMzkZ(=>0i;_ERjT&H3(G&^9490yC1nU&VTbMmty#Y#*e7SFcYJ6-8D>bqTSxnq znixQRa(b%w-)lsYUIf3oAuiW8!3n|?+Xa(uk@2nFPKgsz^2{CSmU=@mp2m|NWOuZC^44n);;qo!?*qP5Y;9-FX$@VwH zNjyf~{aa^ewmwvw1bt)KQbzY8_cTfc-;Cb}>e2cBpopw7pA)9kl$9-!U1|Ves`Sfr zz$F07sz_j%plDmhL(fLIu3y^aZ2dFAE&oW_2Eqf&_Ceh|dv)SmIc-T4u~g~Cpr|_L z9~gK%+hQl!Dg=@Qd-yO232CGM{GZTAcg<5{-MYx8<`vLiE-wz+f?E0iV(l%Xs{Y%p zUj#uUM3E3l0|9Ai5fDXbq`RcMQyL|dE|Cri>F!jzySux)pNqfyzVE%y+2_R><8e5~ zQjx{_)iqFPlhN6L zpuMGq3ONQ<|FPyVmEG|{#?Zavou-D#zidjts+%W65pGt?lGm@`*UnyWY^?voXm0O>|vPV|;mm8m%a_imYRKn!57xb|CCePk;V~Y-5_1 zkH6TyZPAEmzg+%kD@pKvenElbq)X`cplpQ#o$A~EL@bC`H{TA&W@np_FP4NvM>E^P z*0CHHxGo@d@$;YFxzBGfu!vdV_tpRF-Mfi^u5jEv76kN~j!w#lI4-p!3sC_9JqwE$ z_%|j@OmmKiIg4$rt-aa>1jQ6;^i?LA-aM~wYA6^3#UGPSyMI6c2nMNh5*^QvZ{s`+ z?r3kFpUqJ)#>XZa&6B#dZ-uqa8K0TNthZeYqO^v3KDBbIWQ9E)5E0AF4Ebz-{o@>q z+LSnV&5k=>g2~29m{(OSTL~-HWvrv)0<5%cvNS9PI$z?NPpT@80#Bf0gK`t)50{~d z$zXNW!lI~>#0&3Q5^}z3HzMMfHg}lOgfsHi zq-O6xOH%Shu`xcKzpwZ5NJeUL@%Qesc=e%7RZcT?mto=`(VlF#FuTUvmCGu7LE@Oc zCWG&b_2?GMK;^r=SXr*vSN1P$J1eu4*h*fQB**`XPe@qYKi=8CSNGu3?`qra{`nWU zHS`!z-Ec8D($sS3qB5qZ0|AG6;do)DmWx?hnZH}hi?sV4Md^py`9+6_C~g=I>A1@> z=;(tqy@1gG?(RLyWTc9F4)b<4CDaeUjmZ+eh@o+qEoO6|eCG9N?yu({q;6cH_6k#CJ318~EE|a{G6~m(?kab_3A!&9SoKSa*a$MG1EQcJmVBGEiH=#?{t|Z z1+EOPN%)i()*X<}{&~DT{<>mT2d5t=fZ3wJ0y8!LHbDr#(^2{i*o&GJzfIdud zTk3YzNrAUg?4Q{lCkRl`2`D`ScTiF74(AERwp>ov3Hs@iB5&06p}<46v9u(YYO--i zJX*KM;5aNWAruB+UmS`HK=?RRAMiDmTQ2#xn=Szo8XBzbmS|&Dr}BOe8BbRS)-n^JAVF+aaRBFf>&oggj^CFS5q`0!*{ss;U(*vEtfuK*&g zE_U@=S7)cw+YPYU)9zdJ8@~1~BumMGk>PIbGh-1DcRq6M!8g(!{Z8ep#Af=NqsF*N zR!~sz8~_>k#Kd)*?&BUxFWYD^|5XbplA=(ZdAnycSy&d6mX_%Ys+T#mulzWg53m8<3bT8g9ROI)eWYi%eL~7*)0`_yd%ixF^8wmcW&P9kp5Ycyf{|T#BmNuL z$grY$W)+v5JbAFO@$lCAOh5w{dLM4TX~;9b3FQfe8|$;P-^7aD5+2#wP=*t+bU$6QfUI*k zVRoN~JZj)i%2ur^wj4SH`0M-xb=}cLzVo!&w_x{KpeJ!!uhtK8vpXKkFSK$f-ocVQ zhP7@l;DOdwR{WMGcf;D`rTo()AC9JLI|x(0-K_yk5Q@h|whcht&1bVgnKTShJ?UgET-t_)KE`~nscsVVo`<8z{VXsJ1aAu_eJxh`se`$QuW2x9(*C0-LvC!K zKPT<#r`PN2m4$|+=OTjbyl@{sk1U6$E{Y51MRx|k@6Qgm`#lbD3_CICpG{Rey`{gg zt2tYL+x@et3t|3?7wZT{;yJ1+rBOG%xGn7L?BX^1!7Xx_3i$U58{;ptwW6(pFD%ie z&O6xBlv~Pcc!&J@#(?SaORc6_Ji1fGVtx5VZ?Cb4h`ameABjKzBm3gJr469a^~14} zSQHeNgiSJiR^#zkhKDp2MHxexDo&^Eg4wKw{qtE;2Mc@nDH!pc_%mj%_&B`(D8!=k z`o!CiZei{3?z*e%9ACU+E`V}mQ!{Oj$NNb7`Tnv}MK>Mwh)nWY1q6#64W$ z#aWW6xxBmt)iW=@<4QicvIki6@Gt{pY7jo9iN(adCk0O5q@>MT%kTMh5>hE*f&v2j z2RjX2gIP+v_sc8x1|L~knt#RzIZ^unxvp&Snjt>BK5`t=8_d%9sM!N*{G_@S(o$7` z^CHTTa9Sxu6N;lIHKwaEd|KBVUm-#KJmZo6o=;|WP+UU7?NuYDdz^sfN`8Y;!Pfwv zBl|5MM+;08Rr*r22^B81m-pHoDWlJ9NppD~eS;OEf*QfAfopl&=Q}d`+S*|e5ht55 z6t<=6*YjtkokPu3uFYKGijCRj#*uhv$CJ~-=-Z(>Q zZEXc9x4JDa(I0tD!wLixarp;^{l{q-5}vmXNxux37EL@SLS1uBO${%w%>LR9N>>jL zJpcr_yO~jC80s61mf!mzp=G^Z%Gedhg@0b_78kIUPw=LoDgLno_r;eC7=Z}g@ zw=+O zfO?4pf_4KE#!KsUGjOM{4+24`$HCzwm<>5wGd22ACVr<1>YY3K0NL~Ym~nzlaF4^f z6~pSW!%?xjT*%3N>At@EL}MtoBO`;mygay%Pk3*l*{t~m-EkbeH`{sZx4YfVVfQ`J-G^YH2%wdsGPtuhUe+HJ z8HO}cZZ%$)^|83AeU9|$H0K80snFS`&$-7v{+ypjuu01OBhHhY-vfu*#q7B2n+LZs zIK2+DC5u*X)nF-ms}JBPhQ0MXFZp=UNHXD2Y$any zpv*fwMmR&S>;^B6ZU^Pj=H%OxT(pzVIi#K1C5uayU$-#2o?OYOBdh;zROml`^E!IC zT`4dq;`TxFZ2^C=7|yz(j$#6;YEt&vDVLGn+2+*rfpl86JZUSUAGkC$?Ihf0k=#xX zoi&gKNdfL^vlUK%c|h3T*V{@kY-%vL2?#$uf$qRx-f%^s>iZGarvmf8FZlJMX_bn! zG-plT6``QqOi9}I#h1yDTlg+#W`FQ7#cPmf^!Y=kk|bnb;!=KfKFzUoF=XOb80NO| zlVuonuj~8+?()gV3^eMfYG@>qUMZv}O2*DqEj}K0+mF{?`@U}M5zPh-6WOOWtan>w z1F#5<+n0O3fB%k+&7)Ky7!{qUH(nYPT^a4=#ktmWdA@+ox$8c|W&z0>SM+fn79JU| zk?6X*eA(AW^OWizqieXAj1AT1z$C6(GI@swz;tw_8s5+@ruGm2Cxp8BEdey{;s(-` z&ZF z0dXY%K=WeLsJcc_xfF_?=UfqE6J<(PS5E83;ovNkZGc0GfIuMy!*!F2DE0BpTtNMW z{puO2pPJ`W#d;w2N+wMCb8lE^=z|~}X)BPv1?q~A_9Bv_BzU{5tmq?ScF~dOr|BRrnVQ29}d3wGDIvk3OJ=`#P19#-bv3=;~8*qEwV^*YIA09UfSmRm?JIf zqvW56JwWl0)dq#XZzKzsN~fc0ijkQyY+bovYap> zVO`y&cE{Sax7)Wpm7bB4lV%vgkr3piP34lb$B4AJrrmp%)yPwA%V0DdxQV#N*PU;0 zU25SCeRKokrOi6(jq}+C^g-8&G%5cxN^X)(@=1#(FN=yEFi;pT(i8ERpAYAqpiVWx}ZXGIzSR>b%~u`q&yz)uE|Rd@G;1_bTdL*mIemni|7pG3$?JW;iB;rMV4O z4MYg0$4cUbWcaee7iVJEsDIyZ^pY2Kn!L*o>^}M}WgowaGQ3N{8AqQxZn)2-UHb+= z_qK?SwB=V^%sS5J?q;&-u|y=B4%VZ(cV+r>i(qc?U+iLeXJD3sjB;~csAFTPc|)R^Kc+Pz0EZ_lW$d-4h$}5mQz3G)BPdyiGJ& zs{fX^KrUYC`Hl~xTAVIbZ=wEuYwLqsm0$B$LF#sgl<|Ct`>RD+^`nNX-+sHlq|>l7 zUqPdhg@L)&jY((z0Zx8?zPJ>vQn7O3@OLNRRj`H#e?Dk!tgp`viL0yob5pEI*nY~b z3I$P0zCq#k`gxg8015X?Gc%sGSdTuteF1a1=;xDi`K^Ki^Dnp0jw(aQzM*YZskerH z`7u0v?=3%=GCyv+d>+1wDJpt?jYL3AWN00*}&3Lpv2@FBd3#GJbjFrolPeKS4O zOT!K|M*&q{lCjg%S8RV^kX&~3x>)}BTBswEkTRzN?O~b}%j*?UUnGzdaIbLSTp;ljJy=Nn>rG;@t&B=Y<5@)^E|M8mu@02_C z4L*NVUG!{NYhSg7y~Cuv1Gt654Z;dP#R{7ZPiPHf#_>(nA|!7to=UBkm47=;%3V(U zEbe$ZPgT)j0eZJoQmF-De5=hE60XHnd<*I2J+TwSCVb^nP{ zSfpqIkL%8`G@Z3>lW&=g+(V32d7u4&ZQ3wAj%D;EX4AiF0idWsDlHmu8$WrR_}m;H zCZ(veUU|;G;sL{c_0+d-Kk=4NRUcTMGmw~o;d9TPel;V@O#KZLXkOMw`tV_6=i}qMw5QWJudJ2M15&D7XATxgEo!7{>

4lW(>BeY~zP6T_|ToM@O4XR3c?{iaz1zi0=i3 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
Sq^!q7Ei7 zAL&G%mzP#*f?Dzk5y~J|Tl;q8#b3 zZXo7!m1tVb0q{Whusrm+s6CvJL!vl3_Dk962j|`*vh!EEkVK@n*FXN?*@_ zI^F*yGJ; zt*sX3Ia%2e-a0@L{ETxPXx+|rs?cCyS7MC(S%tPn3>YDOS+=72tyJy5JQ%QYm?~FU zKj1rv<oS(LCf*TN>AT;s(WxaO6#d9hz%KZs193LP5 zB{1;Ts?_WLop66hZ+{7ePk2Gv6*KtRV!MVvd?a@t@|q5AB_SBs+8;?t z5kRsJ8NusI7EHEmzGc44Bx>Tz3^SmD59R5s03kXl75;3sFQe_-U*BUusw^0@{sO~g zi%LMBHAX?|jn1n2af^Ekl785&vHBdvq5LNrD=MJ+k1!zQ8J!qAAnSQkHY;)Kfczv=tsCbmIm<}wMnW}#-^WS8Ce z;94*h*DyhtgQEbL#w&h|+Xhn+&3r>P$k|O=K06RcwSM9_EQwu^?|>b0)9$D5_Z1f> zbZD`1WNh^z?|C0&bE#U>fY`tYU52zvy6{eYZ+BrJTAZCusjdi$9w)--W_5Oazn8-Ufiu558 z-iz^D$f^dFv-iMP)hN%j1x6@h*D9~}tsK`-Yz9W;XT?53vK~}radB~+nNigvHd+^t zY)Qb4-PXD9h-1JQsIctx-rhW=o$z234ofSU!TB6Ngy0i0#Sj_}K)e;nN%fGzn}GLo zyW-+|I$;rsHpifp?>90X&a?-GBHu*utpHrkv8V0tlGNU-tv;c`qPjE}rDo~Rga8}nR9=|xS8v>nFwb?j8l1Y%8EihwkXgv?zdl@0aHQPf-%yF^!W*wbHTBqD&d0eoF}_q01Ywces5#@*dX)z18PQU1mC^b`Yo&C7=@IrP`aVknjx z>9M!yp1F2p<}tj&QCKCrt{E;4bq;IlfF3E-o7kFgFsQPR9e3Ppo$}jrx*(mTga-$I0ylSgm!zZ`Y$UxZJUqN)xV5!a zP9Y4#`^aUv3;48+BM8rdP1=sBV33_62`~J?;8&|j{7bEF5pjRzA7}w zP_Yq*s3;a?LC`AJXDUmx9lo!-l&6tBRHR{2Pe%Rd@jYj-+j=Un@qk{%pF=sp4fT#)iP6 zme!Ma>eFW`i#p#Y^_l>62AGq|W5{pa;xVY|`t}BOa(a3T>(s_072X!b)LF9diUo6@ zT}i3)ofN@5t)l^o`Xqro-iNG8IQ3tlinG$e#DZn?L*xs-_rqO!UZjgjPv0fOV6a1> z-P>7;aafB*9zd6(9VG;w9xUq=>ZUsq8cGaR^h!YBC9SY0Yt1+do4HT>4J`I_xWMEX z#z3vRG5rEC-@9pQ=UysJz2C`A0cdX%>5JZ)a7b<*!SzKDw{0s^VKz*ugKvzZ7dEcE5IYk3Mqz4ZW*^*7ztN!31G6vY33DysjP9^|CiYZ9Z2a`5x?tlqy3Mn{nW zD36^BZPo7ZUyYstH7?6Q$R@eAKyvNM*RNmM9f4Di3P>&2AZlK4I>P{~3YgIT#L4xI zVaL+SN=?ZZK!DR3TO2QG@%2C6gZ|TxACG{VDL(%G(iZ4HJA92fQWWRpi9Y~3tNN>H z)XsC@uxLA+YgFOAMZP5p6g9G5D*RUXxwW;t`=MD%u`n(p$)`_WpJo>6RcLD;v@f)F z82m+kgHCdjDnV^Z|A_4PTobT$QA#cH2CS17+uH&ro(DJ-*p7}>7YiOs&I@; zNlDqi2VzO~x<|Hvf$wX$K8#V-8H8~fh)ui*AZ>8&`W;A4IQjhphyp`t1YU&c0ow9F zk!|=@QbI=}S3%S>pbCo((actUaMg1RRQDve@a?UwEFjK8QqAw?;rSP``@LwXMj%Um z;iV$xzFArZ&wGl(*xpl>`irMl9Upg}zc_?3XC;E*#(Er3ADV| zKImIQTU-Th--0kz@1s+sKaywQ0d&!T44xpQMhANIS+2i7zWa7JEQpH7!2!RHGynZn zg#wn-9iLBE8q01}4y=)VFq`&3G!^xXB`huGi^-3-5OAi+ImLlla0UXzF(Tm&t zSZU>8W`3ofixCtR9UnZ*;&26t@tJjzKx5sWz;N**2YOFpJU4L{ zB%Xc^X#Rl1Hj`4^ph>33dIkF7D$4J0HIfm82|O%-s)00b(tDi15kTSMc(*=WcU2sz zklXP(SXPoNxMw2d*ss2UGHdPmgoEU2WmFx4NNza(YX3;F386s^RSRENs`uzgbGZ<@ zt!zqx$9BL(6he#Jh$EaS=351AKl0h0JS{Q+_V?QLfVC>>eUgip>lB|-t%Y$L0a-1u zxb0(r+P@|olr+3&lwAWtF#FeFF6MKzoi}C9Dl0OgoB+f^>7kG0DuF2`NYk~bA3uVg zzuWaExiw#y_hpNMDL5%1!G5O7D0Ua@$=H{mD2-7V5mn1>^SYTdnqBEmt;C6;V>I-Zfu1$?}b{1%~_Mp(%m(-`LeP-o$ ze3gC^Clq$(z1>MdS!ZCi>;GDw zPB_7-x$T3xxqTQIP(WuY9XQhFffS-!&;9%90il1o1Mn6I~KlIEkxiHdq6__ZRzXrtF6@mbZ zMz%6`fn~HpBT)5sAnxbPN?0px0vgD)=b@&bXYMj#5`&NU6kfmX-A$7lEE#zIwPS>_ z2i|(E;iD;t!?;Xx&RR5u)S-Wn?~rkl3A=_9mJyTz-Slv-v28O?#-fJ9Wc6b*10erD zEzd0|i3%M$KEq?_)3<}bPN{!7!_EYFExzVofeKS|M)3z1C~T9?tnI&OI5_NQoy~U= zynK9S>RoiU*v>-2OB@sU&<$r;pONw`4tKkm$6E|8swdQlchPvEo)0gXEP?eA-~7l- zsUD@I7u*kK=9psZFsqB47UdM08O3k?i#aw z9i;-S(IE}zr{i$P z6hc_NLn_ylPfv$~FqS+}@sUlHTiAXgzBJRU%jk!A5 zYk7jZa(qvno&G3qCjD%lcBAKv29Uxga5xQ3dgkoxoGfHDy{MpZxRNkR^}0`1LxhLH z1)tv+_a^cUDm1P1Lz9%^3sZysg`QE9)=s>3jd~W)^Wr3ScABJa0xrGvbe$fUuYwhW z(N~gs>^Ki`Tyd6b0VrREg`B`-bai!=mHu%Ve^Yq12McDdy~cT|9~;AMfCW47e$Bc8 zLSx%&f0?@gdG!2O>G;4Dm*C@qOTtedl{(H)`HV;r&MSb8?Pv~#jBE>JK|L$%AT)Za zf3V9C_ve8_ZnHebxWdxjAN|lA9Kl9{n3}`b%yz4o6R3N3^{cOlX4!%oPe!J>kD+zk z!!Bp~GABt1gIq||!ai_{hykje4N>pG_?F-}-?7!;t^|I-s+3|BHs2roZevlXJk~Eq zP8&(2m*>o+;NZNdk1#emqI)ZnIN9LWYDGqUF76xu)29n$L7M@&R~(m*`%#8kvM@~4 z&1Q?aF%9xCRzxX#%*AqIRF|WceQfm&N|8I%W$UNnd@KwOyZ1dS#+xvWJB6Yj%@L_${&?uI{4-n;BU#n?o7) zNWuLoxsq-5Oj^;y95?FW@rj9%0u#qShYu*)7kdr+mUg3A)ZEtl75Zd_6?%X`fnfhj z*sV%^P`|=5C3Y?lcUVyo+zB$)fGY;&R08q$=f!B!5Ou3q>`=UkoWgb#!U*XGW8eF7 zJzBd6kKVQTsSX85R}p7UZCPd`$3(%4(|rs%2AlD~SllQnX2dc2w@uQs5Wkb?m|=Qv zFI@dwn#^5pu18ys96NP5XX*`ZUC$aTw+iMT0>0(^R_)^Ue>6pK zj~!hWJNBMMB7dZbhe~wEFsnQ)QhP9E%`D{MafCnD$e9FMj_7e-xQsR^d)7U)RdJbW zX~wkgmO~OKmI=mBrJ$FlriTjk2AI2JR;VjM_Ub^6$_U|4`%U1X{-bW42NqRLsigBI z;49qzEEIyStkv?wF6R9*YP_i2USGnWB#S{GKV#G+WzWve1}y5VK@c$?j4OMR^Gr4B zL8WbYfmVUw`?g10@y2O1qDl=g!-q6?IUSdtLxE9D{%U@6KB$IcIknX5Z@=l!Gf#DF zIMr8rO(DP(flL#nh4xSn+eeu0c+9vd0eRT?1*8hxrh4$=`kkNP%}PSYRL3;H`B#z8 zn9gWOzRze1I}4fL%>6eO@M#6WA43SBdyCzD{nPyVnxxx)CQY~P$ScK@&9MTj=g)s= zI=Crm$zd-taA|8jj*auUgmUSVl6efP9~BK|v<53B+79IvxhXkB{(O%lw;wv>$k#5& zCp4ztVF|P9j8(bl*mCq9`f3xVr+lWT%a}~R^PU~jNO&uwq=BVn9+y4v3TO0afuf^@ETZ@6SMGWg3!&)_DTAmg%HW3nV8293R@+ow0h-$nlnmEs{o1At$Z%s#@ zh(o7gGVhK;eQSS5fBCDThc+Gn_+#P(d3>9DQ$W863wSe75|OL%qcQlb_Kq#9$MHZ9 zcTcy$Gao@H2M5lGcMVJg9wjMsz}6>#r&=eA;y?@i0c_^3$bgt^V`rSCf$ku zgC05VJLtS43M}`tr-ZKenD~k7CRXwge~2yAGxw=UbJlUCDo|49(qebN)DgqBl)aI+ zI`6c$G9rcSoYpF^#II2EX4wAue3jJNrN9K`aPDX*G4)T$!pzG0z6E64es?lO*_DhA z?VTQ`xq}sWnhhY(X|_7W`nvJqIJdfCmRH}4S|*2#26XGakGk}hZ*3A@&ix8t*JR}Z z(2e_@q@fWLMC$HDGrWQyuH@^pFLE-IU~2X2&qv5*cpsg&gzpD}G+;Uu22-3>uSJ8-pVG1m~zzv8l@_36B&v7r)&$tbwOTHQk_M+dSHh#A4QAwt$F$EtoAp z3ZH@sZ9kn_$WFi9Gf%K>NMK?7$ac`-0u_^611y@CM)6U#;w8Lwp0$E`HfFXN@OJ!V z5)e4}nCl(|#kx~##vDO%AT7z9-Qzf}N5#Ay`*Yb{)}zKbwT=hYzrWmF;XRZn9B!(p zTty>tn5MuTX@K)8DlL5qq-R(j5>*iwC;a(UxLDcOTKBEm$`HI)ey)jf$Am!Dr&eQ`8 zeC$q*lC#*0{TAG}N7>GD^hE_HUw_wwJHwLEHZza6e;<4%HbW1LjWrBK?}CwaE`ZR^ zFB6-Z-X&F1+DgmHf(=gcnTOQ3X@vR5mFQv?e$X$ny5rALNrEa738X;_s z-Ky_sa!U4pYir9P(nEgJ(>*X>GaN+X9L_dfW?QdC)u3qIf+9|bTXXB8~$o(84uw@*k?EFXi zgXTq-#^xxR-f8Izbn^2XiGtu2_)69I*>8O+vH&g%N2Gaplv zUj2TO>G8g1c~YK1_b%1bM^Ae@I`pAvFDk~$5RT_*$w(Jnm4^Z_XYbEumi!C+t=U(nybml z$qgY-jF!OU>+4Z<_2zr!Ko|2~=kNF1GvP*8aRPfxJG57dM_j723O7uuXW&g%wL)aB;geD0I+S6Dz$&R$%f`J)z*muYNcX$eAqx!}-k z@tRC><}5Bf9yl80Y7TXx+$uu3`su{Hl{@F0IU$&?u}bs=mt#X6pKDi1#{9W(D0@*w z#kmH>6NQ(^m+IgBu$p$K901RBdc|Moi5|~{)Pq@CYgIcaD@hkMIzyt285e29Am9JhB~h}lH!O5HX{TS!#;d{3>kQi^xhg*^Z(kNh?j%g49$nfinwgj_JF9a zk)1rq2)AMN2TYg97U`3#E@OF_+Vr%P7cOK+iS}bZ`qK$^;<6r}D)AHR0tMmJ$4R~m zo16AbFGli?Hw8cG;Io1*@HQ}O+3GFxJC{D*ZY}G^^$*;Ri>IIxs1KUtO@w0!qS_mo@4Pk1Q(OY*Aid;=YV{Vys5^srul?ri1`xGqdnZ+NZ{8yWNPl_S z<$8$UqqbaU8>Nf7%~sN+P??F*-co*yohUGm*=3ATR$HGkK)Ty+5-+w>3lUKpi)(?| zWtP3!1Q^kxC4HQ_T_T?Za2r==N{r|{IR=J1G0h4=@+qQ6V8?|^hSNzz9d#4V<>Cjd zx;~uDzGK7XIf6Sci%~DN&Wn-MS}LD$3yiVGbX39qNv8+Fb$hQ57CkZ0ITR_afRXSXrxjS^d-kytrxkcp6>YIP16TXdY_ksb|T2pLedCsFSV8Qi@af%4Sr%IE4f} z*i@?95Bkb2HYE^%uNY-Fop`pE}P4o5f4wOnkSkFCQzM3?{K@z)uV8OIu=92snHS>9g}IfRLMI z$t1Xz!ijS})#;Pq%Q42)7U&f19UK>o%(ifS1CJ_-D+zJV=WC9{*dHvmyPWLK0sBg~ z&B4bR({3;_$%5m7*?0G1W2MNbe;xsa{YTEOnF9hJzW|p(aiI(jSR}p=ucJVP(GFDj zpfUDhfV0ro+Nwjo8hn=ni2F6%R^v?k%KRp(z>sZYtiY$LP_HKi_0+ASGG8lS9`~BJ zjTgQEAYxvAen74{Yw{=2!6Wju3hwREE1q|>Xt>6ij}#gB9CZvyf0A}vZ}5~EKE^dj zd2GMP2{QtS3fscwG(5+AEebdd@Rw@(Un4vP>T4H33chsBh({~x=au%Tbd~_2%OB@_(6XSfvtu4*W*BmJCLd1BXvS4u=pwz zHCgKL`LW~-L^OjcV_wDK$$ordhqIzWK73WUF1W4nf8 z%OBX@c3J5v(5&fiA*BL#=sLhMO0>2$>H(fu(ca`M_sNE1UO+5|0j57o2}-Chec$)Q zW{t0hukOLA%_NG4^jrrJQZok*R(eVdkj5{}lIvwP3bc!WyENH(84BXfsg*qlI?ODm zp!F;jkrlYz|6=bwgQDD`ZBfjkAbW!n1Vklg$r)563uu!w5+zHPj0r@L(B#;XB!dzq zqo6<|L2{6sGc-AMzuE4y-@Wzj{qxQ_@5igU)~?dpG~NAu-&%9cIp&ySv^tTIWtuj- z>2|Ky9ccz=Wo*O)+Wljp&+0qWy>!RZ#!Bo;CsNk~sM+f5^vXd` z!pe_)``r1nYvX<&9H)!^z>U%b*o*QzWolWW2x|T9&Z+l_14=s|0VpSpjNvd8HOHvi z*enBFy|>UO?ZfgoxyqHxpsvPu^-#pGAn%g zOP^4IDkwqeD|VR=ndLgiOHZm^t?ennZRP&nDV$oWxvDGu@^mcs$$I@Y_SwJmPmgRw zG?q1st&Y~p84B6`&cLOAKxnI-c~-tM!~_ZyFvqm+<;Q)Efpymnv`HKzW#1JdIiBdf z)I7e1%fEN>9mmC5;8hykuh#EA6%|w;igg9?%UfS>A+8?IB^FgL8Jy%brSN8hHBDwy zlX@YhJMwiPmR~#98(E7-uyfSjQ;B&33S4@D8`L6P=GOTt>_+C+=C~Zq``3HGa;fl~ zF%V`uX@xw07o|6&f7L%UO!wRY`hmJ3qd~y7LEWk_CiXGnzKi_q#%!$7_EHb~R~};S z6vdNx6eKYVu|wuF<9?{m_M?Yt%%f&vyVPAm{K8Go2x1CEp1!TmNPbT(sG}VR+B`#N zfD*10;}?By`(G@8BWEXySk(IGTPeR-;_KAPv(%%7KGlw<&C3q)7|@{^91M_ot>JUP zYuiYisfr%HOmWPPVmxv?P8}(?1JOZ;{+f7v+tBTy;hv=+S;>D;d&j6m?r!hx#ZDFY zf`nlT6f#WvAv#nHj_hBXar^s+A623it7A28FHARwFk!CX;DPPqtamz%8XGuzu zTg(k}FVw_@hd=W3lR9Z$1vDega?fyCsPMhfRe^^uw@n?^Lkg*;N#2Eo92+%q!)6uN zUf0pTu5c7~L$cRsyvfCuPLBj<^VRz#g|xG#riS4bGh-t0%HFH_#=u~5?x^-X$Od$V zwIpw}ZcL4WLYqc#k=*+msVO@0Kf%b5oZGP2d2jb@V}PWCgM*mZ(OJ|jj&{l$HzEe7 z-xw|aN|!^d`i)xZC={r=$*;;T2njAU--4EqQKj`8Usb}#oAn%<^{fcdn1*t%b(KZd z5mAlY-XMXOFStKE2a&v$K1ciOs?Vs9X{zgW0HynJA1f`>i}e8u3QqhFI4=Xj!$Lh0 z5xQ+qFO9C)u%K2q|B`B$W0&X#lEF@-%BTD*p8X*|k8KbJ6O6^->CY`ZAvK2oiq<)j zY{*GpNuSOzlG#g43Zhj*e`va*ktzAn?-5sYJfN@3%>__G4&>@cE4=Qd9_Kh=s}i+N zqD!pwn6>~g!$=7J2QwvOobQ{fkA1wodoMYABgv=oixig%rAVl6kngVC>w@Mc=mkIe zXd5q_uJBS>@p-89bvzM9!9)0&3DiT_XF#viSekTPvh{LXU+m7wn<3jNC@NY%bfvy@ zjcXgd&Ca035VMw1y8mNQ>(}Vh!}CvC$G&1$dx{bht4kG1ER?6WRX5s9CK7L(b@F#z zgf7y%HN)lou!mkHf9(TD@G=Eg6xT_&3>EJdC^XgZp0Ir$$n?|7ew6yG)@yjYA<^zp z^ke`p2MbF!aH~KP8n2-^c0Kkk{i*ck5cRQGuz`G9PkFq%yEZE1wykX>RbRe3Jky54 zm3`#YEp3_dqd zdb%7*qL7V_HiZ*D$X>&)Nz|!@bOJ0~XWz$kv!VX!uQWVKRgw_wpVqqSA`6o3k`G;@ zgb*1|pB!X)C(Vef6pcw-gBH|G17p{I#urF5oSoU#szPYi>c6h0LkY)Qc~+jHO+ec4U;!6-*ZBX)n;t=W4 zWS(lG?br9qLM@eHPP1)wn+4?*+=k#>{~)hOk1_Mh;SKR!U+=Z!15jwq44*yaA9gkN z?isoViq&Tm@QSkjs&uC)_iz3Oi`$>X|G!aY_P>W90T1|pFLwI@e$qh6phr;7D?}I; z@)-ZZtFl+rW{ovY8E=DVVpcH^@ugt=3h}?n#KIwaMe`bgEcX!huZ(g*;!DghwoY*K zJ0Vdp@;~GcrOxW~=!#>)LT{hO{|H7$-g(eaa{h;=;;F$gG=Emf_Z_)OJi8>^MBj%p zrS1Fuf8JFD_t%We=eKYF7jZ=33=DxbVi%7VG7K~h`@{vcYQCwC6Su_;KU66Z(N2s{ z#B+HXyyz>1SU5!QaQx`uMx;4liDI1MazX0+ys=E|3y>K!m$&oXvuaBB&!!&x8$pIA zr~L1f5yO*C4IX{&G^HE=^vSZ~)W|ZH^|&42odz%R+@{xT?#EGL?@Y|i7guH$rdBfk zksho??Vmk=VZLR`+RiQ$-j{$?`svfSd1(ikAV)`)bb>qft-F8aGpqXWeRRKtr{CO^ zi8!M!^3Z2xIt%{}ny0Q1U#=)GhEZ$$6X5;imUx4}^v8PYA1$i?j6m`Ko0Wq|`t<1& zG&uBYJz;zpzQK%XA>|9`rq6>_JV3{aiqRoTUtE{_q2P8~A3G^y5_!JyVc4Mb>94u= zME3O*6lxswRe`I>;5dLrTMvGEGJF$A{J=2YKRkq&2rcqGJkXlv?VvASr7Ihar+t zDVH84;iY*g70AUA)-xdbar*S>VvBaR$d)*W1)#J!WccOTy&vAtEY8cb0ptwW9ND&!N}UROFo$HTZ6{T1h8JrV8IGpO1cQIC`16gMP=|Kg+1MmTeeY`dreEh>saIhi z6B83u5E~roLL=!d{52kMi#edb0F9;tem#hkF+(wbSIaUEG;~-8WuZlA#nK2P4pEFs zF~Y%|sN0wG-D26bRQi>uOF;rY2yHi9`goj6w^Uy8GgR@2HNma`kUWd+Ii;K^sF9^x z>6BU8NMYn1q2CUYas(6f7}oJKD+>$ShRwf!ivuyn`i};Gc*bvFv?U8W-lrZK9UUDS z(g8_+ASM9260FerKioT`Ac@G^-rd!d!&~1%`;*=o`fUc_$b&oC8y9n5Tky*cxvB8b zaNLRT%~^3eIuuCUM6;^N@(u#0y7ZjytVy=Ggaot;4WRp=;1e*=<`0hC+y+(Zw=-v? zK|DnvWq7o5iAX|RoB;tVL(77No;oEF77&7#N^LIP4YxP$x6}pPm%R6QEk7EXe5usZ zhJ&s7&SbHyh3=fBPuY*t13+$kQjmyw&#qM4_ue5fRg1i|j^ z2UQ@@&&;SFWQI`dV~S#7SyZY`+vkjZ#CUB0%uAF4GX!#GnH+f@fT0|eil7Rq2~1wC z!q`*D`AqX(L}inO&UwN@xXfXqzI({WK=HLVw*U3(7s6YULKRUs%t^b{^;7g+;Y~Wr z`#NF@7-~V=a700`Yo;W}DS;rnrw!CX|6EN+AYlWvy?TqtDTU^gnebs^ZayyQiQq;^eEj_CW56+88n7s%Qaqzrft~9aM-T5pE8lv&uGZyel89RInwF6d zATuY2JB-F{D!0FnbV?pQRJh{MRRd3nKoUX6L@WddBLXd7@yw&XBdF>d&7 zZRX|G7f?$vIRiZmr0c2q|GDSAaL=Ker1Z(pBq5yc$ubnYULtGf)36x6?bh zhukAgdT=QV`SrShb%yt&01D~*MNvJB-wO0A5o{HQB(f^DwM5!5wh?s~sBqb1^5PI6 z7*yg0HGNH;oMyUGr6D+l7XYZkxcWjPV6n)FR7>iOtvUYQv@WapUW}2&Jbc;8Y}20) zsV(US!|-ftq?})PhKU_1H#aw-fprB;mtnWUGia9P=P4}9momS#7QYVSsi8#lMOhd0 z@@1Z`@!YXK;dL-s&P0mZ9i#4Hc6VcbJnnITnWL!KXcA!05f{IP39Cr0#ZW%}uYxYm zo^@&J7-ot=5H33YknzvAp`%T|*nk{Yf$ahj7P^8`-usXI!mqx_l+}gN0^!(o#WCRc zW0`szt`|*#m!&dFxBgX5F;`LQ5qG zvjQ1e@p&7ZVL)3dyN66Dw;$C_dk`cXiSKn!jXTtzFkO2b)V!1)bj^StxK3jbpEB)N zW@gj@EEayDSn%OTQF`4bDbqr9a4IO&0>9!oQX~8qQGRSLq_L#O{rNg%SFbWSfYo*a ztYm8-%7fB1$g?Bd3m&Du%*)HebjTZM9ldAF5eLatS<&HAD|)+Pc%8}jcM2agTs~_; zEQ2w$KBGbesg?91Oiq2hLaT!fAf{H-IF1gEXqp3RA;E?tkH@RT`(!SM$I(-7wpi(? zTV;S5EQQ@q_wB@=g9k7*&IK|Z00{yE-|-{zE#u$6zJ9&+)g$2bYlZ>(1R}l~%FSA* zTB7hlj=pJOS4`L&e)YR|{rP)sIC0B@uodK6CA(i~OqgbkzR!+mwDqX<9;FYq@tm#Y zdw1!XNyeyZinva$dQOw>xJBd4er`rE$U>+BSb{!RUD{sW3_tnc}w4lgbbB)`L} zoDz5|hKLeK`5f{~2N1y_7t%A{*SABAL7?|?3h`>r1PpH;Uzh;%6Ssw~TfR7$@?l4% zsk;8roxRB#GR4}3e-Xj|V#M%op0n?A`kA)&Al#`!!`fViNHf&rQ51#(n{IE2|2?{N zlHIEdkaP$-jQxgBZVDoUv)H~rKYntPk^4Q&mBB%JfMGnod`#rm{x23VXJ}}60hnMr zJCMT^A!(340j>61bM5%!D4x&q_}+Q)vCdA1=1^MDRzxzD6&GI@w2i~K5pe{ZY8|YB z4IB{-QPHQH8AOJcVOR96LDqmSd}=%clU*4q$yJZjWfh%3z!DCxy~W0}6TmYDd1gFk zeh)qp;&@V24mg>>ekY?&fOG?UawH}q!rIC(WEr;-N+-qjmE-G-mGLA*vfYizMkeKi zxcK<^1g1ttg7f5!{${pg7n27PGSqVrcpc18a8i_$m1Sx>9yjK}-(-ngYv_IdMy+}wHp}Pjxo2l2qlwEISYdom|T>vyBa$qzO z`Gs-{GuwvXjUMFC%Z2j;?3XoMs5=@=YH)KBAISW=fYHIQ)z+^H)KqBq16__m!oVeC z8g3FH*9>Gk84tiN!5~xd8Vm|Ew!#xDP2~uHEt+54j}5Xj$M3VdPJBmznMKV^>?|k* zxO6B`Id`T=Ad@q8S-!TmwJn1bGT4?P?nAv>`ywYTEzRscp@p^mFJ|*6ghUA0-Hna# z@N@$N_4Vs)_msxw+G16wBCnn#B_}f`$!GA?ybfxAlmy2W4Ez+|>C4rK)M}M4mbcdD zxzmDAnqY;=A>z*}`T6-N+(TTCCed)AAfyv>duX8n>^eeUUZ8E{tpMiL)TFJg-7=N) zL?b}4!c12|^Pg1-39QqlyW|MT1*p8cIa@V}U!NzJ?oj@jBB56v+9U`=7l1vt{Xovb z!LeTA!KkO0tNJW_#h};_qeCjkDAo3jg5}X&lNu~DSkU1427ZJfzl|S;1)8{$Wc~Ba zG#DewFCKn$o>Dsm9LVz8A@MaBT4f|`(`^$cw#DB>hP({A{L{~~801=(r?Y%>Xjh{5 z*3PK=!z*iH{z;^%2qI%t4TEi|&47x><#^YOX}G4%K*go-h7z|`-Bv!*)r?~1=qEW+ z^_xU+5DjOk(zW^`nj#5Ud5Xb*r-&wsGDsRWi&k*=k~%n4^mV=_O~cH0a?_GFnq|Lc zCwvcv@ZpkBRkTGihdt=e;HYWp0EU80MWe!RM0MtLbTBxOa@jDr8W6risKl-^-YS4X zBLG5~`>&cHVo}Gb*RNi^;;2z~WNZ(;A)$L%gFzlafNhoRDYa6ISF{lJ-hW28t4K>G$f+e5$C}YlW8OGoyF!A&FTX>FDTyV0-xX^fQbu5h6-b2(L_zmZJBov z7#Sf?45f?cn zr0?IqI!bb`jVLphW3)(MWFLi`AR7N3)&nfx1t^#BLyYls5}w;-YpO9VY&{k~;~uju zb#p9@_BxbroF}LE!yd!Z8-olM#=O@@tC>faN{WcM15T}1dH`+oGeDovc>>E>1<9&= zug)LRx3CrIRjk2gTuM*vlTiGKR4@bOrJa}OW>;H?%t31lyi-dTPLB5Qr6KtB=4vI@ zGg`I6!)n{TF6xqv?J0mRK{2F*@2;^gjjzx`YDBNp{OL&}jQ&EChoGUw+n<1=FW373 z!&-GSO3+UCM33Ld3eML04SzzZhzrFwkP{%BYaOaHUCkZ+3#J*GFm%z{zK~NKY+So(zX&Ur9aaUTSLEQ zOzM+AaPh_@L!7U)bR!V^0fg>kfFUl0D`TqOr(eC~`{-O=UXFkd^pCpNT{Pd=yz3^& z)nRq@{~?2;qu;~6ZcuD+dKKBPpt;;Xdf`d$9~U1J`PG3TR)Iwde`L)bK^y8Cwiv$~ z2oOvu|9N{NhWT{@FR5LRm1!7CDWv2XO@W3^Y(%f^BH-7XN@f$NU2Cq?eEi(9>I~+q?FMKh* zLZ+S+_mfNgOB+q;>YHT4*=UI0%$WbrFJo(f+XpxCl1t#?c`p9wdl^OyV)R4HL`6*S zKE{+JU-2i<|2IRLlV>hW0D_w+ZfRkm`N{aJ+v;#Kl)WB8b=bpYfYR9MQemRfDh={z z_Exsrx2GUc1&MgYOKykTRyun?++lBBqHW(@ZBG`{BLCUergYE+n2Qgc<1kkB;aD5mssNk(^hCPM z>t`Gf(kqaNDy^^knRSUjR<50(5b=#b`ceIub__Yxtink zM;$J43D?`o_ULx1@F3Q5iiB4=)Lh;Y#Hui0whW2}S(Zt%6PF?R^(SHcP8$qGTqP9v zP$l*p(n$I0U#W(%B2cFI1D)I`i z(@U)sC1Yxs>;G)>J;Ot<+_utgMDQdPvaN8p$T=uQSKz|UmV0wGlxP`<2lI7G;8tlC z>WcvJ9?DGUgvY)tiAtcQp~+|$vK{=;E@x+_b1wdOYikZnJK?ZLMK7Wa;JpDVWHcpb zMa;mbN8OE{cPIM;jR2Hn3`!E{=Ej~LwYqz-8e-hAYt`#!do1eN;fFG6)VR&* zW~ld(WifW108oO_5`aeN@^C>K0qZFIGN>_PO*>5!2!6V?-T_lr{>1`nYlp#rK|sVM zrg0V)GA-y40Z$$I94c3qYYc?fZrr$0kHc1H=(k#sYJ&RBRT`Qa>pnJDU4@%Q-kWb` zE)hOQ5-{@40_J@b1@4#JFd+}?9qGY9uVC^U9uqvH*#-RoNzZNh(_qih&d9&J)W`lt zYc=CLpbGmltz?AM$Z=4w;O5c3E~oqyNdj$+rP*0Vkw=)gsD4q7P%Cz%A+vnil&*+?i6Ve7eH8#$ zpp(V+08;>C!D|LO3Yb=?WEqh;^kC}y;6CWlzj09thv|Sax4$ix`wJL5<&D**OKD=; zewD|(@H<>&CN$u!Jk;h8p^2T~f&Bycdk($)ZcJF@w;be@!Znqt_@wLMx3g-ZuNPG% zSX-&{Ch{*rRz1V;vL;%VlK*Kq13xPc9e=L1qWcjcjDsC`Kr)Wd!X2&G&la4eRN(FA zuqJ@uSTt+)g1@MDZ>E3&5g&$SJ>U8qp$6|C)eM+T#I}&?a!8@As_PuQg7LS^venDN@3%k5&O)Li*u1s` zC@{5zXK4~9Ts4eum|aV*#fn0Qb&*oYULSbIbDb!ZF@bM^D{eB7j)5fw+D5gD28i0> znxN=1wJQhbTPt<|%ywdu8G{J!4x4F-zPQv87`ybuT(mHs7*2LUCYW4g?R8p%_NJ#uvH7Xd%$gc$3UeZ{BG@=fylQTcZ~fLe6JaNRECy4_WMr+ zgF1|@Tu-P`ERECx;z^J-3@i7)Yemp%sD&%$en80g-nhgmw?wM6J)Z<61fdMNd7|OP z8PcWRZT5^$OquF-*}=iVW~2&n(1O7TLN5&9CM9&wC$LA$_}hN0hKiDs#UXQtLB7Z+0 z%UtQ25DSZZZqbrn4wvHg`?qhy$*2@>Z&yI$0(vpe-(97JV;mmn7M6hs;vd zBwDz61kVQ$56(URl_v9!YghiW%2F+<6N9?~odB-ob6M}LvIs^md>hv*54G(y({if_ zsQkmF5C;1L&^#3n9_*xPipa*kav*$-bT0iHM1LY=yQ31k@oi94X2x@{Kp-8P>Gk*R z1W?+Fhg&Iz11XKi5@sE9w$X8i?=7IOagx!(>b!qnPK76Xr*^-c9TYaohYgx8)J1g0%ed6he{bBGcSWoI^|0i+^rfCa&Z~K3FPOvgKB8&A7rnesjngPLikH8Li>B zqq~ zj#Wq@2i@1FQ3V82`Z8A5^M!z<9<%^LVcwCX)1H~IA68SCMZf9$?KL5Owzbqbn4B|; zp#|vLhmq<~9zPwcIRFeEyFX1LqI1({M!IY+`X;jgUc`mPVFrLod;=TEzK5D@RZ#V+ z{$j3vJyVFE=YzaOv3q5_-UzpAJj4iNURN!X%#4y5kao;vCOQl!DVr4rml%||m}&99 z4<$b!vP9H^4}X;vV2*9@9fFLztjm)>p!XIzHS_JwU zTei91Y3A%TFRhazbwSmK)3qhzryJDz2vmsFNQF_daD9EWOYt2@6MvN(fNxH!bYN82 zJqYQrNY|m!gr?9$??BiA*iECpC_^0F_7B~jGmsJ zY^;N@$>-`;@K~>rk6BcbSa@I5%dFm?JKX8fcX?s7jMYm~35^Dts!p-utMkNWk6`RQ zV}Xsw0x)e~UtdQ_F339J1ciph33R@o!IP8X`G{FPQLFszPff|kDl{!0f7$PQpMm^T z>SQ}IDgnl3ndr9R>;lWkJ)L5+AijhsYjexET>4i1({?={mCe~!Cp9)(r&?7Q+qk!2 z=-UhCucvTT-Fk(g91sf@`Q6YDr^$3j| zn2&Xaxa^qn!|chETSLzq{7;b<-~m0eD>TB7BxPvXEXgxGa1grcv4=_t?kj_zft=zy zoh78u4-VB|Ufbn#iHe9Q{FQv?$^Dn6irdkAHzB8vAtQIXln(K@_|o~)8*rgg9F9!3tkIacS!0oDV0mq3>bz z)$=LGG)BjHIb>TFd0R1yS{{AnN-s(|qZQ@w@CBjNP z#j{{5>H!ZB+N=0&4SmT7Qhxj2+8!4am+>CM1RuaQ0{{1aNlO=RTdb!O5b%cm5tIyf z*xB2f0?;?9?wvwLN36I8{0jw~eg^W|DI~Q)Zr!Ir4p~erQ^oeIC9_p_Gyhd&1iTUy zvEZ9D+;L`%|B~T~Ffp+o_4&)ZEI!`H8=LhHYs6_eR&OUjNb|1ca$

@Tj(Du);5mbi>Fe)T z#B&{2Nf$nS+DN>y_XkTVX26s5CSFCpLWuCYDz7{BOc&DRv;FZCHZBTO?G_gO$4PoR zYWVBcFVla#pK1Q0F|$wk>3$Ut`8srd`zC(#j`klSWihDp$H#9zw1OXG2*bA&PrK&- zt@9>+kr9NP{nskbzIyJkQOBDm)?)H%Ad(x`Y@T?zgM%NRmKJfZ&-BR@^BG z;|9Im`3F1h|6&15D)Bq(NL-XzbV!M-l(YSMoiv)d65h)8EmZbr0~P(KmK^6(UOu3F z>fLCU=u+qF%S*S4I+nRaM5I0zZ+2`Ct1DZwjNrI4?ONo=j$!~WC7noZ_UOcOYezqO zjqe(Got#ObwXq+qvzeGM(ogCse>#@!JDLEhVHYnZ9qpyr2-!0U_jY#rb*ZLz7}ccJ zB^;?XFft|@OHR;%b=pJu+*dPYCU0MgGwt= z@APr&UQc~=1swD%G+iy{OP5AO{7&>fdk<}z`f831vsUnx%yVUHW}3W|9#us}4j>qV zSsg4JKNcibtSA~mQKqU?$F8lnMOZz4Y6NK~`>k}{y z{%FMN(DZDY+I}P>@<4cS#BQXt8zfFdndT;?*ID_n+GQ5fJ8ak3qqx%a-k{V)`rH$6JHzUGS!dJ!P_riNnfFp@Lg%jfiVwJ{aOjV_kLQ_eEX}PJS65-Mc6LA52Ymu~S`OG%r0zuI zm`Qr=(t2ZhnZ?ER=QdhK^r+e7Cc$Sy;Z=)ut`8W@poQU!JGr($+a7GK-n+5Djm%%~ z7(Eh~pkwlh;3u@d-b%1^Zlw6VSx51`X$pf`=gf%AGV`H~6K=z*v{gO@tJbFMp_@iy z`kNP8zv)*xDW86Y@t%(m*xB{1F&`-$saVgRZn^Tw0e5SN`pjw4g2=YvQ7^+Rff};9 zey#-ll0KE#{i6#s!chx>Xtp+MNif%_&q^3qjOMsu`IEMKZ?w|ONZ~&DrAwD$isFvq zox96*JA-2jQ$qGKV+y#_7nLa)mT%*xN^1@;fMnJE^86L0AfLj+c4V~jM#tro_24Qm zCF&nka=Su9+r@{#n`yb&?TOx!(7|#kG!GLen)8I-kfO45(6VGPI?I^qtWiA3tO7DD zQxyd@(S*cC5x4wU%=$9uZ{J4LEq2c3h&4X7dUA{UzF~=(TY*tU%=X^a-T-R%JbUgk}GZ(le)@17!pLb)cAZ?3zaN zIES9-Mpw1MG)WLgppXPN!j{%*3@`(r7Mn9UiHQiH+2l1s)39cmm9Z<(8F61zh zHK1U%29)6pNCY9CLiD6ca3 z%ox|YhjFF*CT|a{SS{-7n+bqd(Bg4D1px7Fr?O~nlF;_9mmHP}*$)ez>{Tw!U>xg1 z4;6+Lt8j5!d&p~fjrSWflxK_&1M4KN3mqCE$Xho)9+=O3fguw6v8t{5zU4ki5cH(v zbIKN6U2;wQ*d9mE$&6pHXNI0+5Y#zuv6XqvuboLA zu1FEyad(}M7jgX!NNIBj>UJ=EW0%I49(-gqD{B2J?_L+nL}{(QE-DLV8nEr_VFcES z!^mf^mvmLQh6oc<0~GdYobyR!V#;@uN+ao8o1m?C-96=B-_*>_s=pu(>4l54tIdxA_IhPUPW&_N!~ zN6>R+=4ZSCE{W%H;v+XqYY$di*dYdksbo7);-I<379&O`x3=bdufv4zafu34^0J(AVl6LwhJ z>sWiV)yuDqvPv#mI1?CHusqE8Y?VY4{oGGxmRmK3BR;Y8DG0+(=8*je*>|0Sfp?r9 zuMt+A4HDu7AH+4|6VfElpUlm5%G4(g>!D?2Kd`rDE{ilEnxm$_bc`OJ4GchkT}YKk zr1+V5E3lGTwbDk0l*)NwgJ-qgs?u~TH(Ci-Xjs<$u|TXeH^TeTqh~FPCG)t0<-PYd z+3BbYqyb?AlVstlk=Gg|G>oyKlqVy|*}T9j$G)BA0(mjmXeo!ap)GR7RhhYRn>K4IxwNvM)*Jqk zj^vUmlTRnp%$+Wdr5faMF2}5o)|Fe7p~;v!y9*Vg<)zk(WK3m=qRR%Wp2&>VRBGkx zGoOt3_Lxi!{@wj~1|@=Fb+ysZqOe;1QDEvzRpmGDDe|E+Mxd=!XDcIQ3oXN2W7T_3 z(zkoGPavIt=bHy#iFsSG-TaM$;);#&Oqfb%n^ZG+!xqiHQ1o&#hM%LO?}B2Dgqm2` z%~5b-eevR+UO%@{fS)CGCl6&g&iH=R(vA%~az7L$?v_-DPm{H1xJR=)8aY^vF&$x=Xg z$^GJ6+a=+WIr(m@eK;5p$Xx2ON8$n^6LuOk3ftH}TQQMr6{v1SzlF#F6+X^&zx^*{BR7 z#?;_rb7!20`@OF)kUXC5S>xWaK zb`^XuVH-+!;@rNZks-?zcv0lacSe?zdk)KL|Ze>kb zSs9J!K%s^*T9QOu!l+U{K{%QLM+2&U(Ns*4ZyV(p zp^*)Ra^X;}^sE>Eb7NF&fUXWYFNuy)%CX9uuG(dv$uwXaVYB+HAU)$NltEU+*>e>X z)Fy)^#J|AUD2IlIbmXsk+(B$T3is2hKAKYvVWFM-;FqkCU{vJACC>AB+pWC4Y0FB- z)hgxKBEv{EYK)f29NktLeY!q$%H{{rA)nu2 zZW5sig$fi7~6 z4x`75L;0IHGh95stLBn=j+uQmNY?wklLXF&E~h` zwtEsp*F%$!d}+G$u4I&^Iwcicvy9=;W4w@$$nRqN;am6F{fo#VSA|-b^3m>6E^UGA zlC`pkvsrM7JQN*DF$IMhweM)2*hp}4nv!(%>c)6J8Z7E`N0OBNODf2XYyFMt>iw z9NN0v;)+Vsi7%W%%NrtI&C1 zsMk705IJ0r*DSYUu9dTsk(F!xyQC^xNi$z@ek!g0lBOm%Ocni)^*owsO6?lHa;w6A z9CGB38+m5c4Sl1b2S@q>y*kTp zEO$<+RPWx{*!JW%#R#7;3%16fcUYy-Q{&;3L)KrQVLBZ{JNz1P%FW5eyPw0}GFP+k1MYheJyxi#ze83p zov}^JTpm@($iB<}FBYJX-dUDPzOCt5K_ z>yZu#gs7&Lrrj~{CV$2WWNm7pl#so;>pGa9@wLj$gT^(+Yc6H8(~(jfqesUJNk6PG z4(S%hJPvLBqkLJReMa|08Y#(djMX`F7pP`lnq>+-lBq4-|0hRzE7cBSY(YW^lk;oV(ojbKn#t?j38OWa*^V{) z*>Vfef!|-utB;-CRE~VY9QwXJPMX!CVD+hTaZJ#%7C;E_*2ou)@!MM}l5<=Su;4m}sKk1o`#%P_Fa7ZgQJDi4`+yhN450zEux4zY*R@ zuXmv>`i6r;i+O5?u_`|C+i737NHdqld{lETb`6T_4o4oVbh3Ifhio&tb+C8Xhbw6P zijHH`=-G_4Xfe9eit79^V6ahay60>4UT!RGr(SaD=iaIm$NYXx%?+xzesgo=V2hwU zzrsN%(jax`_HF&@5-ktkfX8g5Wj0M?mutP&vFC7HHjj!vR{4x$IzLXX&s7oY%{t5- zzSrBC=Sbv5t#urq#k~W8(UO7u5r(ZIodV37#Dy9s552>amHt>sZ*01w2a&n#VIMtUz1nfnk^O=sW?@!R;zL@QXucpP-itSt9 zX?}NH{fTmz>vg2}Oa9`u{$h)Q>l&B(-Nee{CrL-mQDj*i4--UfpnQiMC5Pcb?!su@ z>YXQ;pAtt`e8=;av5j(B4{^@j`w_!(EjGyseSOPv*v_$J0tJmxF9Q3s7jpGY%%91b zsgdn1IEN|y^*c>A-12}wE$wue@?Vc$zkTpG#aXykibv^xr?7l@dHaDHOH1AQ>3hB3 zZWfbFe0rS}AM=r~M&T6N< zkqAVlLXQLU)sb>^&CEl~UtL7HWgf!%W{XEA6%nYAMXdgM;7FNDAsfM8l_!=&&mvAW z$W%Q@J&Mj1e0{qWsPt;iBd^!ywz$c~514e4T06PpHl{~rN9Lar)@J)U5IyV|MtJ+% zd!pHHyL=;kD$W};L6*pZ1{CyrQvlj@!F-+&|G8$K`Rs;qb*@yS?3| zG34Z!{txaY!~|C=ZKgjS_l78@wz@82kNcbYMaznm>PTa#UalpSc^Fg&vh((-IZEKF zj|FV{Gc5KLt`aHZ?g%Vvx9&|c^U~_1600Whm)TD$w^0j`Kdj=b{lvpuE3z?q?lOfr zH33a%#D-?MjUj8R_3nId{mRkO@}6K0D)GptLB;+!uPFC#E*5@!ZSJHqsFB^@I}awy ziH|aM``Co~t9Ikk!-#SOMA{LuuGR+b-Gmw_kKH#f9n0Ov1Tm4S%^`_#x+;gt*yvCx zE_L_jkmNY;!NHUyLHFmja}Gq+&yT3+^<$RyethU9tl8Ibna%AZPAt20VFl$WwbfDG z=m`QyvpI4-Jr0D;yab=dM^xDwzF#1SeUw|Z8DJw_ZZ$mhHKC7t2!u`Ic*sJ9YmUL0 z3;8|1Ym?Y-XnuAemfOffroBxPcibk)UGMh%;Pym(gjs9Vu zmeT!oLFV~}N0$}dXmHE3+`-Fmz!zj(#M*!VcCcV}c}SFQHWUpLEZ3 zXOdiuC~HAyS^9VNU1{_7TKM#?S>s}K=CguZ9%3fEWBJ;jF^iU-*rgCPYVxtaVU9Wb z(LK*>yS5q8KvhD6nJ~HcIl7sMY=b}zHq_D91>DAhva5Bn>ztL^$M%ICzoOsT%T+c- zl_r6C>X<^-gIu}nvjIhSp3CoFYEE{-DBQ}c+52ilW>t|9Q^P3d&0l;%N9UkkZy50; zK8NH|jwvu-o#%XFJU=^SzI)PqjgBB`*ymQk8?C~sG|yo{6!}v8_xk6zd;X^-+`1mX zf6eW;m~@~<_OqW>Z&sOYmyC>Dd~3xn%i2(g`~^4dW&JjFSApF6^*-){r8Y+H+F_~J zGm$Fdx2EM+5@L<6bJdA==d^JO4P42wSU93*QcWJKZ8Kez?qa9$a=GXP6o*T!Pmlca z9j&t82tNJIo5YWvp9^T(>7TfjT&^6ef$8#p5JAr_Bt)b0#HwK3#%WFD+_m*j-^v`m zxJ^vlYS$~v(=5aM_PC6sc*T2#;0k&+ia6fy_;u@(fxyuSPG1W35@)CTZQ=(RWWKR( zWAfws(d@Kz)}K=QC6;07d}n4@>y;s9&?N1l1`l&kLR)3jn6`r{}VN}`m?de!9I-&^hqrJ1x9oo zW0$dY3*WXnRLn$!157YIqn?`LyMu0aYt-1uaoMo=uH~VQ#R^0B)m?9#(1pF#Q3(x( z3j}G!D0Tl}5(2K&)8r=F6& zk)o3L_;~BWJHOc5XZ&APuh;C**4Tirn+oV4l_*vGAoO@9wb=da^FK63fpWGU+Y(6y^kPCZvUb@t4v6y;{cgD2$HVPEwK6^D#Yz;#1_j~^Ju`BPbBZ?1Vl9`)`krG-7jb&eo4u?} zVZG1)JmHvcC1Qwu)p?_x0@=z8t^Sz;O#of|`QOl}Un$|2sJUa94xt7A#no zIG|v5^C~pXBJsf$RgCG+N9@`m<@6)YOs06A6H6P%{a@qfv0n6k{_m~T3x1aW{CV@) zbgRc-f@hoTdA*sHd$vjb<%bu~&EB5#?)K{HBB#ewHb^bxySc}+`N^D(o{F1IvrGI} zL~N_x5xjX?t~l@vq|5##E9!0@?etNf@aR(inq#`R&djm>e)9OO<>wh{@*h`AnXcHL zc(`pso&K&5dv@*D-;p=zZFEd=>DkcL*Uff)|L58*uDSc;WMX2?L8c{F`gHZt(p7sc3zGzU_wtM(4|F_dGt# zDr}lQ=jrQ1+~;`hfv1X1|5FMa;j*?d%isNVKkyF0Pa7Yrw{3p0c_`8jKx$@OZVt4Qq9xlDU=hAZbi93$~FaD90N^3oI^8{l9M{WK5SMz3D z72hj7E;I87zX@>HU;ggRfv=zS=J`~!WiBd6i2~N}JsT6lTTi{6=q@8@HO2bp_k|rB zQ%~DpcGsMqb9UC&g+ijA%C^}@Y2zSMrBJ$2R^_o}Jkj8p1c#25_v-zP8TyF3IlpTAy`bg)wk_}Y&+@n6 z_%q;U2qzByn_F8>{W%9bOuYEzCH9n>IN1&Ne;1v+WbA+UeJX{JozIToVFMyq&xI8xQa% zD3k0NHK`}64_lYitcsoeJ1e%{H>tceTc1DHwFe~HBT4E5Jv_!Qnin284WR#(i>e8 z%pVWnVOLXcl%G-oW`qaFH}M;9;6lz5(F=h2te{LGf5Bhn zuUbe0NV@RNXB8dL5pf@yz3=hu%{TtAA86YD|69J@0j*?b1LYWIhSsE;)f;9%ISmwH N@O1TaS?83{1ONagrbqw) diff --git a/plugins/voyage/tests/e2e/voyage-playground-a11y.spec.mjs b/plugins/voyage/tests/e2e/voyage-playground-a11y.spec.mjs deleted file mode 100644 index b8f2bea..0000000 --- a/plugins/voyage/tests/e2e/voyage-playground-a11y.spec.mjs +++ /dev/null @@ -1,143 +0,0 @@ -// tests/e2e/voyage-playground-a11y.spec.mjs -// v4.3 Group D e2e a11y + pixel-diff + SC24 XSS guard specs. -// -// Tests: -// 1. Light-theme axe-core scan — zero critical/serious violations (absolute) -// 2. Dark-theme axe-core scan — zero critical/serious violations (absolute) -// 3. SC1.6 inline gallery — data:image PNG rendered via scheduleRender hook -// 4. Pixel-diff smoke (1280×900) against baseline PNGs in -// tests/e2e/snapshots/. Threshold maxDiffPixelRatio: 0.02. -// 5. SC24-security — script injection in artifact body does not execute -// -// SC2 authoritative verification (axe-core). v4.3 Sesjon 17 (Wave 3 Step 5) -// converted the SC2 assertion from delta-baseline to absolute zero-violation -// after Wave 2 remediation (Step 4 color-contrast fix + Step 3 sidebar -// toggle restructure) reduced the critical/serious count to zero. - -import { test, expect } from '@playwright/test'; -import AxeBuilder from '@axe-core/playwright'; - -test.describe('voyage-playground a11y (axe-core)', () => { - test('light theme — zero critical/serious violations (absolute)', async ({ page }) => { - await page.goto('voyage-playground.html'); - await page.evaluate(() => { - window.localStorage.setItem('voyage-theme', 'light'); - document.documentElement.setAttribute('data-theme', 'light'); - document.documentElement.style.colorScheme = 'light'; - }); - await page.reload(); - await page.waitForLoadState('domcontentloaded'); - const results = await new AxeBuilder({ page }) - .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) - .analyze(); - const violations = results.violations.filter(v => ['critical','serious'].includes(v.impact)); - expect( - violations, - JSON.stringify(violations.map(v => ({ id: v.id, impact: v.impact, nodes: v.nodes.length })), null, 2), - ).toEqual([]); - }); - - test('dark theme — zero critical/serious violations (absolute)', async ({ page }) => { - await page.goto('voyage-playground.html'); - await page.evaluate(() => { - window.localStorage.setItem('voyage-theme', 'dark'); - document.documentElement.setAttribute('data-theme', 'dark'); - document.documentElement.style.colorScheme = 'dark'; - }); - await page.reload(); - await page.waitForLoadState('domcontentloaded'); - const results = await new AxeBuilder({ page }) - .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) - .analyze(); - const violations = results.violations.filter(v => ['critical','serious'].includes(v.impact)); - expect( - violations, - JSON.stringify(violations.map(v => ({ id: v.id, impact: v.impact, nodes: v.nodes.length })), null, 2), - ).toEqual([]); - }); - - // v4.3 Step 8 — inline screenshot gallery (finding 31d28f65). - // Injects a pre-built artifacts object with screenshots[] via the - // window.__voyage.scheduleRender hook (avoids webkitdirectory which - // is not programmatically triggerable). Asserts the dashboard renders - // at least one data:image PNG tag. - test('SC1.6 inline gallery — data:image PNGs rendered (31d28f65)', async ({ page }) => { - await page.goto('voyage-playground.html'); - await page.waitForLoadState('domcontentloaded'); - // 1×1 transparent PNG (same base64 as the fixture file) - const SAMPLE_DATA_URL = - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=='; - await page.evaluate((dataUrl) => { - window.__voyage.scheduleRender({ - artifacts: { - basePath: 'fixture-project', - storageKey: 'voyage_proj_fixture', - brief: { path: 'brief.md', content: '# Fixture', frontmatter: {} }, - plan: null, - review: null, - progress: null, - research: [], - architecture: { overview: null, gaps: null, looseFiles: [] }, - screenshots: [{ path: 'docs/screenshots/dashboard/sample.png', dataUrl: dataUrl }], - looseFiles: [], - }, - }); - }, SAMPLE_DATA_URL); - // The gallery is rendered inside #voyage-dashboard - const imgCount = await page.locator('#voyage-dashboard img[src^="data:image/png"]').count(); - expect(imgCount, 'expected at least one data:image/png in the gallery').toBeGreaterThan(0); - }); - - test('pixel-diff smoke 1280×900 — light + dark within 2% threshold (SC1 backup)', async ({ page }) => { - await page.setViewportSize({ width: 1280, height: 900 }); - // Light theme baseline - await page.goto('voyage-playground.html'); - await page.evaluate(() => { - window.localStorage.setItem('voyage-theme', 'light'); - document.documentElement.setAttribute('data-theme', 'light'); - document.documentElement.style.colorScheme = 'light'; - }); - await page.reload(); - await page.waitForLoadState('domcontentloaded'); - await expect(page).toHaveScreenshot('voyage-playground-light.png', { - maxDiffPixelRatio: 0.02, - fullPage: false, - }); - - // Dark theme baseline - await page.evaluate(() => { - window.localStorage.setItem('voyage-theme', 'dark'); - document.documentElement.setAttribute('data-theme', 'dark'); - document.documentElement.style.colorScheme = 'dark'; - }); - await page.reload(); - await page.waitForLoadState('domcontentloaded'); - await expect(page).toHaveScreenshot('voyage-playground-dark.png', { - maxDiffPixelRatio: 0.02, - fullPage: false, - }); - }); - - // v4.3 Step 2 — Group D Playwright XSS injection runtime guard - // (finding 1d3591d4). Behavioral counterpart to the DOMPurify fix in - // renderArtifact (Step 1). Injects a markdown - // payload via scheduleRender and verifies neither a JS dialog fires nor - // a \n# title', - }); - }); - expect(dialogCount, `expected zero dialogs but got ${dialogCount}`).toBe(0); - expect(await page.locator('#voyage-viewport script').count()).toBe(0); - }); -}); diff --git a/plugins/voyage/tests/e2e/voyage-playground-network.spec.mjs b/plugins/voyage/tests/e2e/voyage-playground-network.spec.mjs deleted file mode 100644 index 5d2ad28..0000000 --- a/plugins/voyage/tests/e2e/voyage-playground-network.spec.mjs +++ /dev/null @@ -1,33 +0,0 @@ -// tests/e2e/voyage-playground-network.spec.mjs -// v4.3 Step 30 — Group D SC7 authoritative network-intercept gate. -// -// Instruments page.on('request', ...) to capture every outbound request -// during playground load. Allowlist: nothing (zero external requests). -// All assets MUST be bundled locally (./lib/, ./vendor/, file://...). -// -// Why authoritative: voyage-playground.test.mjs already greps the static -// HTML for http/https URLs (Step 28 SC7), but a runtime intercept also -// catches fetch()/XHR/import calls that are constructed dynamically. - -import { test, expect } from '@playwright/test'; - -test.describe('voyage-playground network — SC7 zero external requests', () => { - test('no http/https requests during page load', async ({ page }) => { - const externalRequests = []; - - page.on('request', (request) => { - const url = request.url(); - // file:// URLs are local — playground is loaded via file:// baseURL - if (url.startsWith('file://') || url.startsWith('data:') || url.startsWith('blob:')) { - return; - } - // Anything else is external (http://, https://, ws://, ftp://, etc.) - externalRequests.push({ url, method: request.method(), resourceType: request.resourceType() }); - }); - - await page.goto('voyage-playground.html'); - await page.waitForLoadState('networkidle'); - - expect(externalRequests, JSON.stringify(externalRequests, null, 2)).toEqual([]); - }); -}); diff --git a/plugins/voyage/tests/fixtures/annotation/annotation-brief.md b/plugins/voyage/tests/fixtures/annotation/annotation-brief.md deleted file mode 100644 index 864b3b9..0000000 --- a/plugins/voyage/tests/fixtures/annotation/annotation-brief.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -type: trekbrief -brief_version: "1.0" -task: Demo task for annotation round-trip fixture -slug: annotation-brief-demo -research_topics: 0 -research_status: complete ---- - -# Demo brief for annotation round-trip - -This fixture is used by `tests/integration/annotation-roundtrip.test.mjs` -to verify SC2 (byte-identical empty-anchor round-trip) and SC7 (per-target -isolation against `validateBrief`). - -It carries no anchors. The round-trip test runs: -`stripAnchors(addAnchors(body, [])) === body`. - -## Intent - -Provide a minimal brief that validates against `brief-validator.mjs` so -the round-trip integration test has a real artifact to revise. - -## Goal - -The brief should validate cleanly (no errors, no warnings) and contain -enough body text that adding an anchor and stripping it back is a -non-trivial operation. - -## Success Criteria - -- File parses via `parseDocument`. -- `validateBrief` returns `valid: true`. -- `stripAnchors(addAnchors(body, []))` is byte-identical to body. diff --git a/plugins/voyage/tests/fixtures/annotation/annotation-example.md b/plugins/voyage/tests/fixtures/annotation/annotation-example.md deleted file mode 100644 index bbbc51b..0000000 --- a/plugins/voyage/tests/fixtures/annotation/annotation-example.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -type: trekplan-fixture -plan_version: "1.7" -created: 2026-05-09 -slug: annotation-example ---- - -# Sample plan with one anchor - -This fixture is referenced by `docs/annotation-quickstart.md` and the SC12 -machine-proxy verification (`parseAnchors` exits 0). - -## Section A - -A normal paragraph in section A. - - - -## Section B - -A paragraph in section B that the anchor above refers to. The anchor is -placed on its own line with a blank line above and below — the canonical -v4.2 placement disipline. - -## Section C - -Another paragraph. diff --git a/plugins/voyage/tests/fixtures/annotation/annotation-plan-large.md b/plugins/voyage/tests/fixtures/annotation/annotation-plan-large.md deleted file mode 100644 index 09a0aa5..0000000 --- a/plugins/voyage/tests/fixtures/annotation/annotation-plan-large.md +++ /dev/null @@ -1,1090 +0,0 @@ ---- -plan_version: 1.7 -profile: balanced ---- - -# Scale plan for annotation round-trip (51 steps) - -This fixture is used by tests/integration/annotation-roundtrip.test.mjs to verify SC3 (>=50 steps + >=100 anchors) without breaking the parser at scale. - -## Context - -Each step is a sentinel-only step with a valid manifest. The plan validates against plan-validator --strict. - -## Implementation Plan - -### Step 1: Sentinel step 1 - -- **Files:** `tmp/sentinel-1.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-1". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-1.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 1"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-1.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 1" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 2: Sentinel step 2 - -- **Files:** `tmp/sentinel-2.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-2". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-2.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 2"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-2.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 2" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 3: Sentinel step 3 - -- **Files:** `tmp/sentinel-3.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-3". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-3.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 3"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-3.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 3" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 4: Sentinel step 4 - -- **Files:** `tmp/sentinel-4.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-4". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-4.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 4"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-4.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 4" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 5: Sentinel step 5 - -- **Files:** `tmp/sentinel-5.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-5". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-5.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 5"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-5.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 5" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 6: Sentinel step 6 - -- **Files:** `tmp/sentinel-6.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-6". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-6.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 6"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-6.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 6" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 7: Sentinel step 7 - -- **Files:** `tmp/sentinel-7.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-7". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-7.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 7"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-7.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 7" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 8: Sentinel step 8 - -- **Files:** `tmp/sentinel-8.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-8". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-8.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 8"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-8.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 8" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 9: Sentinel step 9 - -- **Files:** `tmp/sentinel-9.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-9". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-9.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 9"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-9.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 9" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 10: Sentinel step 10 - -- **Files:** `tmp/sentinel-10.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-10". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-10.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 10"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-10.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 10" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 11: Sentinel step 11 - -- **Files:** `tmp/sentinel-11.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-11". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-11.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 11"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-11.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 11" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 12: Sentinel step 12 - -- **Files:** `tmp/sentinel-12.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-12". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-12.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 12"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-12.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 12" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 13: Sentinel step 13 - -- **Files:** `tmp/sentinel-13.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-13". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-13.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 13"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-13.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 13" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 14: Sentinel step 14 - -- **Files:** `tmp/sentinel-14.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-14". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-14.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 14"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-14.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 14" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 15: Sentinel step 15 - -- **Files:** `tmp/sentinel-15.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-15". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-15.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 15"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-15.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 15" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 16: Sentinel step 16 - -- **Files:** `tmp/sentinel-16.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-16". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-16.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 16"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-16.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 16" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 17: Sentinel step 17 - -- **Files:** `tmp/sentinel-17.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-17". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-17.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 17"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-17.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 17" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 18: Sentinel step 18 - -- **Files:** `tmp/sentinel-18.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-18". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-18.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 18"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-18.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 18" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 19: Sentinel step 19 - -- **Files:** `tmp/sentinel-19.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-19". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-19.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 19"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-19.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 19" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 20: Sentinel step 20 - -- **Files:** `tmp/sentinel-20.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-20". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-20.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 20"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-20.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 20" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 21: Sentinel step 21 - -- **Files:** `tmp/sentinel-21.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-21". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-21.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 21"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-21.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 21" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 22: Sentinel step 22 - -- **Files:** `tmp/sentinel-22.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-22". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-22.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 22"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-22.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 22" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 23: Sentinel step 23 - -- **Files:** `tmp/sentinel-23.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-23". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-23.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 23"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-23.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 23" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 24: Sentinel step 24 - -- **Files:** `tmp/sentinel-24.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-24". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-24.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 24"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-24.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 24" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 25: Sentinel step 25 - -- **Files:** `tmp/sentinel-25.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-25". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-25.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 25"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-25.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 25" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 26: Sentinel step 26 - -- **Files:** `tmp/sentinel-26.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-26". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-26.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 26"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-26.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 26" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 27: Sentinel step 27 - -- **Files:** `tmp/sentinel-27.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-27". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-27.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 27"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-27.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 27" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 28: Sentinel step 28 - -- **Files:** `tmp/sentinel-28.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-28". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-28.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 28"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-28.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 28" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 29: Sentinel step 29 - -- **Files:** `tmp/sentinel-29.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-29". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-29.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 29"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-29.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 29" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 30: Sentinel step 30 - -- **Files:** `tmp/sentinel-30.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-30". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-30.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 30"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-30.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 30" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 31: Sentinel step 31 - -- **Files:** `tmp/sentinel-31.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-31". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-31.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 31"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-31.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 31" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 32: Sentinel step 32 - -- **Files:** `tmp/sentinel-32.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-32". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-32.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 32"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-32.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 32" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 33: Sentinel step 33 - -- **Files:** `tmp/sentinel-33.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-33". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-33.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 33"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-33.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 33" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 34: Sentinel step 34 - -- **Files:** `tmp/sentinel-34.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-34". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-34.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 34"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-34.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 34" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 35: Sentinel step 35 - -- **Files:** `tmp/sentinel-35.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-35". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-35.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 35"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-35.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 35" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 36: Sentinel step 36 - -- **Files:** `tmp/sentinel-36.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-36". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-36.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 36"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-36.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 36" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 37: Sentinel step 37 - -- **Files:** `tmp/sentinel-37.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-37". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-37.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 37"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-37.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 37" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 38: Sentinel step 38 - -- **Files:** `tmp/sentinel-38.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-38". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-38.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 38"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-38.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 38" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 39: Sentinel step 39 - -- **Files:** `tmp/sentinel-39.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-39". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-39.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 39"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-39.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 39" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 40: Sentinel step 40 - -- **Files:** `tmp/sentinel-40.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-40". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-40.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 40"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-40.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 40" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 41: Sentinel step 41 - -- **Files:** `tmp/sentinel-41.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-41". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-41.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 41"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-41.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 41" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 42: Sentinel step 42 - -- **Files:** `tmp/sentinel-42.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-42". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-42.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 42"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-42.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 42" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 43: Sentinel step 43 - -- **Files:** `tmp/sentinel-43.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-43". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-43.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 43"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-43.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 43" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 44: Sentinel step 44 - -- **Files:** `tmp/sentinel-44.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-44". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-44.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 44"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-44.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 44" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 45: Sentinel step 45 - -- **Files:** `tmp/sentinel-45.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-45". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-45.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 45"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-45.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 45" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 46: Sentinel step 46 - -- **Files:** `tmp/sentinel-46.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-46". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-46.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 46"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-46.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 46" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 47: Sentinel step 47 - -- **Files:** `tmp/sentinel-47.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-47". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-47.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 47"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-47.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 47" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 48: Sentinel step 48 - -- **Files:** `tmp/sentinel-48.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-48". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-48.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 48"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-48.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 48" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 49: Sentinel step 49 - -- **Files:** `tmp/sentinel-49.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-49". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-49.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 49"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-49.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 49" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 50: Sentinel step 50 - -- **Files:** `tmp/sentinel-50.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-50". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-50.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 50"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-50.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 50" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 51: Sentinel step 51 - -- **Files:** `tmp/sentinel-51.txt` (new) -- **Changes:** Create sentinel file with the literal content "step-51". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-51.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 51"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-51.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 51" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -## Verification - -- All 51 sentinel files exist. -- npm test passes. diff --git a/plugins/voyage/tests/fixtures/annotation/annotation-plan.md b/plugins/voyage/tests/fixtures/annotation/annotation-plan.md deleted file mode 100644 index 63f0341..0000000 --- a/plugins/voyage/tests/fixtures/annotation/annotation-plan.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -plan_version: 1.7 -profile: balanced ---- - -# Demo plan for annotation round-trip - -This fixture is used by `tests/integration/annotation-roundtrip.test.mjs` -to verify SC2 (byte-identical empty-anchor round-trip) and SC7 (per-target -isolation against `validatePlan`). - -## Context - -A minimal plan with two steps. Each step has a Manifest block so -`plan-validator --strict` accepts the file. - -## Implementation Plan - -### Step 1: Touch a sentinel file - -- **Files:** `tmp/sentinel-1.txt` (new) -- **Changes:** Create the sentinel file with the literal content "step-1". -- **Reuses:** none. -- **Test first:** none — sentinel-only step. -- **Verify:** `test -f tmp/sentinel-1.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 1"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-1.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 1" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 2: Touch a second sentinel file - -- **Files:** `tmp/sentinel-2.txt` (new) -- **Changes:** Create the sentinel file with the literal content "step-2". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-2.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 2"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-2.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 2" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -## Verification - -- `npm test` passes. -- Both sentinel files exist. diff --git a/plugins/voyage/tests/fixtures/annotation/annotation-review.md b/plugins/voyage/tests/fixtures/annotation/annotation-review.md deleted file mode 100644 index c680ea7..0000000 --- a/plugins/voyage/tests/fixtures/annotation/annotation-review.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -type: trekreview -review_version: "1.0" -task: Demo review for annotation round-trip -slug: annotation-review-demo -project_dir: .claude/projects/2026-05-09-annotation-demo -brief_path: .claude/projects/2026-05-09-annotation-demo/brief.md -scope_sha_end: 0000000000000000000000000000000000000000 -reviewed_files_count: 0 -findings: [] ---- - -# Demo review for annotation round-trip - -This fixture is used by `tests/integration/annotation-roundtrip.test.mjs` -to verify SC2 (byte-identical empty-anchor round-trip) and SC7 (per-target -isolation against `validateReview`). - -## Executive Summary - -Verdict: ALLOW. No findings. This is a synthetic fixture used to exercise -the round-trip mechanics; it does not represent a real review. - -## Coverage - -| File | Treatment | -|------|-----------| -| _none_ | _no diff_ | - -## Remediation Summary - -No remediation needed. ALLOW. diff --git a/plugins/voyage/tests/fixtures/playground/v43-export-bundle.json b/plugins/voyage/tests/fixtures/playground/v43-export-bundle.json deleted file mode 100644 index 200fa0b..0000000 --- a/plugins/voyage/tests/fixtures/playground/v43-export-bundle.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "schema_version": 1, - "exported_at": "2026-05-10T18:00:00Z", - "target_artifact": "plan", - "target_filename": "annotated-plan.md", - "annotations": [ - { - "id": "ANN-0001", - "target_artifact": "plan", - "target_anchor": "step-1-sentinel-touch", - "intent": "question", - "comment": "Should this sentinel use a deterministic timestamp?", - "timestamp": "2026-05-10T18:01:00Z" - }, - { - "id": "ANN-0002", - "target_artifact": "plan", - "target_anchor": "step-2-sentinel-touch-paired", - "intent": "fix", - "comment": "Step 2 manifest should reference Step 1 in must_contain.", - "timestamp": "2026-05-10T18:02:00Z" - } - ], - "annotation_digest": "PLACEHOLDER_OVERWRITTEN_AT_TEST_TIME" -} diff --git a/plugins/voyage/tests/fixtures/playground/v43-plan-pre-annotate.md b/plugins/voyage/tests/fixtures/playground/v43-plan-pre-annotate.md deleted file mode 100644 index f334698..0000000 --- a/plugins/voyage/tests/fixtures/playground/v43-plan-pre-annotate.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -plan_version: 1.7 -profile: balanced -revision: 0 ---- - -# v4.3 fixture — pre-annotate plan - -Minimal plan used by Group C tests to seed an annotated round-trip. -Two anchors target `Step 1` and `Step 2` so the export-bundle has at -least 2 ANN-IDs to canonicalize for `annotation_digest`. - -## Context - -Fixture only — not executed. Anchors below match the v4.2 anchor format -`` and -sit on their own line surrounded by blank lines (block-boundary rule). - -## Implementation Plan - -### Step 1: Sentinel touch - - - -- **Files:** `tmp/sentinel-1.txt` (new) -- **Changes:** Create the sentinel file with the literal content "step-1". -- **Reuses:** none. -- **Test first:** none — sentinel-only step. -- **Verify:** `test -f tmp/sentinel-1.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 1"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-1.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 1" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -### Step 2: Sentinel touch (paired) - - - -- **Files:** `tmp/sentinel-2.txt` (new) -- **Changes:** Create the sentinel file with the literal content "step-2". -- **Reuses:** none. -- **Test first:** none. -- **Verify:** `test -f tmp/sentinel-2.txt` -- **On failure:** revert. -- **Checkpoint:** `git commit -m "chore: sentinel step 2"` -- **Manifest:** - ```yaml - manifest: - expected_paths: - - tmp/sentinel-2.txt - min_file_count: 1 - commit_message_pattern: "^chore: sentinel step 2" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - ``` - -## Verification - -- Both sentinel files exist after execution. diff --git a/plugins/voyage/tests/fixtures/screenshot-project/brief.md b/plugins/voyage/tests/fixtures/screenshot-project/brief.md deleted file mode 100644 index d52957a..0000000 --- a/plugins/voyage/tests/fixtures/screenshot-project/brief.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -task: Screenshot gallery fixture for Group D test -slug: screenshot-project -project_dir: tests/fixtures/screenshot-project ---- - -# Screenshot fixture brief - -Minimal brief.md so `loadProjectDirectory` reaches its render phase -without emitting the "brief.md mangler" warning. Real verification -is the data:image PNG count assertion in the Group D test. diff --git a/plugins/voyage/tests/fixtures/screenshot-project/docs/screenshots/dashboard/sample.png b/plugins/voyage/tests/fixtures/screenshot-project/docs/screenshots/dashboard/sample.png deleted file mode 100644 index 0f2de3749df299a6b84bf6ff1a0b393a1c1fd22b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k92}1TpU9xZYBTuKYyVd1A7xwz3mB) Q_dp2-Pgg&ebxsLQ0NDZ% { - const md = 'Line 1\nLine 2\nLine 3\n'; - assert.equal(relocate(md, []), md); -}); - -test('relocateAnchorsToBlockBoundaries leaves anchor outside atomic block at original line', () => { - const lines = []; - for (let i = 1; i <= 20; i++) lines.push(`Line ${i}`); - const md = lines.join('\n'); - const out = relocate(md, [{ id: 'ANN-0001', target: 'sec-a', line: 5 }]); - const outLines = out.split('\n'); - // Anchor injected at output line 5 (1-indexed = index 4); blank line at index 5 - assert.match(outLines[4], /\s*$/; -const VOYAGE_ANCHOR_ATTR_RE = /(\w+)="([^"]*)"/g; -const VOYAGE_ANCHOR_ID_RE = /^ANN-\d{4}$/; -const VOYAGE_ANCHOR_INTENTS = ['fix', 'change', 'question', 'block']; - -function parseAnchor(line) { - if (typeof line !== 'string') return null; - const m = line.match(VOYAGE_ANCHOR_RE); - if (!m) return null; - const attrs = {}; - VOYAGE_ANCHOR_ATTR_RE.lastIndex = 0; - let a; - while ((a = VOYAGE_ANCHOR_ATTR_RE.exec(m[2])) !== null) attrs[a[1]] = a[2]; - if (!attrs.id || !VOYAGE_ANCHOR_ID_RE.test(attrs.id)) return null; - if (typeof attrs.target !== 'string' || attrs.target.length === 0) return null; - if (attrs.line !== undefined) { - const n = parseInt(attrs.line, 10); - if (!Number.isInteger(n) || n <= 0) return null; - } - if (attrs.snippet && attrs.snippet.length > 80) return null; - if (attrs.intent && VOYAGE_ANCHOR_INTENTS.indexOf(attrs.intent) === -1) return null; - return { id: attrs.id, target: attrs.target }; -} - -function stripUnsafeComments(text) { - if (typeof text !== 'string') return text; - return text.replace(//g, (match) => parseAnchor(match) ? match : ''); -} - -// --- Step 25 — HTML-comment indirect prompt-injection mitigation --------- - -test('stripUnsafeComments — drops prompt-injection comment, keeps voyage:anchor (v4.3 Step 25)', () => { - const fixture = [ - '# Document', - '', - '', - '', - '', - 'Body text.', - ].join('\n'); - const out = stripUnsafeComments(fixture); - assert.ok(!out.includes('IGNORE PREVIOUS INSTRUCTIONS'), 'malicious comment must be stripped'); - assert.ok(out.includes('voyage:anchor id="ANN-0001"'), 'valid voyage:anchor must survive'); -}); - -test('stripUnsafeComments — strips arbitrary HTML comments (v4.3 Step 25)', () => { - const fixture = '

Hi

'; - const out = stripUnsafeComments(fixture); - assert.equal(out, '

Hi

', 'all non-voyage comments must be stripped'); -}); - -test('stripUnsafeComments — rejects malformed voyage:anchor (Sec T4) (v4.3 Step 25)', () => { - // A comment that LOOKS like voyage:anchor but fails the strict allowlist - // (missing id, bad id format, missing target, bogus intent). - const cases = [ - '', // no id - '', // bad id format - '', // no target - '', // bad intent - ]; - for (const c of cases) { - const out = stripUnsafeComments('A\n' + c + '\nB'); - assert.ok(!out.includes('voyage:anchor'), 'malformed comment "' + c + '" must be stripped'); - } -}); - -test('voyage-playground.html stripUnsafeComments wired into renderArtifact (v4.3 Step 25)', () => { - const text = readFileSync(HTML, 'utf-8'); - // Function declared - assert.match(text, /function\s+stripUnsafeComments\s*\(/, 'stripUnsafeComments() function required'); - // Renderer must call it before md.render to enforce the allowlist - assert.match(text, /var\s+safeText\s*=\s*stripUnsafeComments\(/, 'renderArtifact must call stripUnsafeComments before md.render'); -}); - -// --- Step 26 — path-traversal + symlink/dotfile filter ------------------ -// Mirror of the browser-side isProjectPathSafe predicate. Kept verbatim so -// the playground's filter cannot drift without breaking this test. -function isProjectPathSafe(inside) { - if (typeof inside !== 'string' || !inside) return false; - if (inside.indexOf('..') !== -1) return false; - if (inside.charAt(0) === '.') return false; - if (inside.indexOf('/.') !== -1) return false; - if (inside.indexOf('node_modules/') === 0 || inside.indexOf('/node_modules/') !== -1) return false; - if (inside.indexOf('dist/') === 0 || inside.indexOf('/dist/') !== -1) return false; - if (inside.indexOf('build/') === 0 || inside.indexOf('/build/') !== -1) return false; - return true; -} - -test('isProjectPathSafe — rejects path-traversal (v4.3 Step 26)', () => { - assert.equal(isProjectPathSafe('../etc/passwd'), false); - assert.equal(isProjectPathSafe('foo/../etc/passwd'), false); - assert.equal(isProjectPathSafe('a/b/../c'), false); -}); - -test('isProjectPathSafe — rejects dotfiles at root + nested (v4.3 Step 26)', () => { - assert.equal(isProjectPathSafe('.gitignore'), false); - assert.equal(isProjectPathSafe('.git/config'), false); - assert.equal(isProjectPathSafe('.DS_Store'), false); - assert.equal(isProjectPathSafe('.env'), false); - assert.equal(isProjectPathSafe('docs/.hidden/file'), false); - assert.equal(isProjectPathSafe('research/.git/HEAD'), false); -}); - -test('isProjectPathSafe — rejects node_modules / dist / build at any depth (v4.3 Step 26)', () => { - assert.equal(isProjectPathSafe('node_modules/foo/index.js'), false); - assert.equal(isProjectPathSafe('packages/sub/node_modules/x'), false); - assert.equal(isProjectPathSafe('dist/bundle.js'), false); - assert.equal(isProjectPathSafe('packages/x/dist/y.js'), false); - assert.equal(isProjectPathSafe('build/output.js'), false); - assert.equal(isProjectPathSafe('packages/x/build/y.js'), false); -}); - -test('isProjectPathSafe — accepts valid project artifacts (v4.3 Step 26)', () => { - assert.equal(isProjectPathSafe('brief.md'), true); - assert.equal(isProjectPathSafe('plan.md'), true); - assert.equal(isProjectPathSafe('review.md'), true); - assert.equal(isProjectPathSafe('progress.json'), true); - assert.equal(isProjectPathSafe('research/01-foo.md'), true); - assert.equal(isProjectPathSafe('architecture/overview.md'), true); - assert.equal(isProjectPathSafe('architecture/gaps.md'), true); -}); - -test('isProjectPathSafe — fixture FileList survives filter to brief.md only (v4.3 Step 26)', () => { - // Fixture mirroring Step 26 plan-Verify scenario: load a directory - // containing the four hostile entries plus a valid brief.md and verify - // only brief.md survives. - const fixture = [ - '../etc/passwd', - '.git/config', - 'node_modules/foo/index.js', - 'brief.md', - '.DS_Store', - 'dist/junk.js', - ]; - const survivors = fixture.filter(isProjectPathSafe); - assert.deepEqual(survivors, ['brief.md'], 'only brief.md should survive the filter'); -}); - -// ===================================================================== -// Group C — v4.3 Step 29 export-bundle schema validation (Wave 7). -// -// Verifies the JSON shape that /trekrevise consumes when an operator -// applies a playground-exported annotation batch back into the source -// artifact. The shape comes from buildAnnotatedMarkdown + -// downloadAnnotatedBlob (markdown export — primary) but the -// trekrevise-side reader (lib/parsers/anchor-parser.mjs + -// lib/parsers/annotation-digest.mjs) accepts a parallel JSON payload -// with the same canonical fields. The fixture in -// tests/fixtures/playground/v43-export-bundle.json is the contract. -// ===================================================================== - -import { computeAnnotationDigest } from '../../lib/parsers/annotation-digest.mjs'; - -const FIXTURES = join(ROOT, 'tests', 'fixtures', 'playground'); -const BUNDLE = join(FIXTURES, 'v43-export-bundle.json'); -const PLAN_FIXTURE = join(FIXTURES, 'v43-plan-pre-annotate.md'); - -test('Group C.1 — export bundle JSON parses (v4.3 Step 29)', () => { - const raw = readFileSync(BUNDLE, 'utf-8'); - const bundle = JSON.parse(raw); // throws on parse error - assert.equal(typeof bundle, 'object', 'bundle must be object'); - assert.ok(bundle !== null, 'bundle must not be null'); -}); - -test('Group C.2 — export bundle has required top-level keys (v4.3 Step 29)', () => { - const bundle = JSON.parse(readFileSync(BUNDLE, 'utf-8')); - for (const key of ['schema_version', 'exported_at', 'target_artifact', 'annotations', 'annotation_digest']) { - assert.ok(key in bundle, `required key missing: ${key}`); - } - assert.equal(bundle.schema_version, 1, 'schema_version must be 1'); - assert.ok(['brief', 'plan', 'review', 'artifact'].includes(bundle.target_artifact), 'target_artifact must be one of brief|plan|review|artifact'); - assert.ok(Array.isArray(bundle.annotations), 'annotations must be array'); -}); - -test('Group C.3 — every annotation has id + target_anchor + intent (v4.3 Step 29)', () => { - const bundle = JSON.parse(readFileSync(BUNDLE, 'utf-8')); - assert.ok(bundle.annotations.length >= 2, 'fixture must include ≥2 annotations'); - for (const a of bundle.annotations) { - assert.match(a.id, /^ANN-\d{4}$/, `id ${a.id} must match ANN-NNNN`); - assert.equal(typeof a.target_anchor, 'string', 'target_anchor must be string'); - assert.ok(VOYAGE_ANCHOR_INTENTS.includes(a.intent), `intent ${a.intent} must be one of fix|change|question|block`); - } -}); - -test('Group C.4 — empty-export edge case produces valid bundle (v4.3 Step 29)', () => { - // Mirror the export-shape with zero annotations (download button still works - // — produces a bundle with annotations=[] and digest of empty canonical). - const emptyBundle = { - schema_version: 1, - exported_at: '2026-05-10T00:00:00Z', - target_artifact: 'brief', - target_filename: 'annotated-brief.md', - annotations: [], - annotation_digest: computeAnnotationDigest([]), - }; - // Round-trip: serialize + parse must equal - const roundTripped = JSON.parse(JSON.stringify(emptyBundle)); - assert.deepEqual(roundTripped, emptyBundle, 'empty bundle must round-trip'); - assert.equal(emptyBundle.annotations.length, 0, 'annotations array must be empty'); - assert.match(emptyBundle.annotation_digest, /^[0-9a-f]{16}$/, 'digest must be 16-hex-char prefix'); -}); - -test('Group C.5 — annotation_digest is order-independent (v4.3 Step 29)', () => { - const bundle = JSON.parse(readFileSync(BUNDLE, 'utf-8')); - const ascending = computeAnnotationDigest(bundle.annotations); - const reversed = computeAnnotationDigest([...bundle.annotations].reverse()); - assert.equal(ascending, reversed, 'digest must be deterministic regardless of input order'); -}); - -// SC6 — annotation_digest SHA-256 validity (per scope-guardian SC-GAP-3). -test('Group C.6 — annotation_digest is valid 16-hex-char SHA-256 prefix (v4.3 Step 29 / SC-GAP-3)', () => { - const bundle = JSON.parse(readFileSync(BUNDLE, 'utf-8')); - // Recompute the digest server-side and verify it matches the canonical form. - // The fixture stores PLACEHOLDER_OVERWRITTEN_AT_TEST_TIME — the canonical - // value comes from computeAnnotationDigest(annotations). - const canonical = computeAnnotationDigest(bundle.annotations); - assert.match(canonical, /^[0-9a-f]{16}$/, 'digest must be 16-hex-char prefix of SHA-256'); - // Determinism: two calls with the same input MUST produce identical output - const second = computeAnnotationDigest(bundle.annotations); - assert.equal(canonical, second, 'digest must be deterministic'); -}); - -test('Group C.7 — fixture plan parses with anchors at block boundaries (v4.3 Step 29)', () => { - const planText = readFileSync(PLAN_FIXTURE, 'utf-8'); - // Frontmatter declares revision: 0 — the entry point for /trekrevise - assert.match(planText, /^---\s*$/m, 'YAML frontmatter required'); - assert.match(planText, /^revision:\s*0\s*$/m, 'revision: 0 required (round-trip seed)'); - // Both anchors present in canonical format - assert.match(planText, //, 'ANN-0001 anchor required'); - assert.match(planText, //, 'ANN-0002 anchor required'); -}); - -// Group C.8 — SC6 round-trip: readAndUpdate raises revision to 1 + populates -// source_annotations + annotation_digest (finding 1bc37231). Verifies the -// trekrevise mutation contract end-to-end against a tmpdir copy of the -// pre-annotate plan fixture. -test('Group C.8 — SC6 round-trip: readAndUpdate raises revision to 1, source_annotations populated (1bc37231)', () => { - const tmpDir = mkdtempSync(join(tmpdir(), 'voyage-c8-')); - const tmpPath = join(tmpDir, 'plan.md'); - try { - writeFileSync(tmpPath, readFileSync(PLAN_FIXTURE, 'utf-8')); - const bundle = JSON.parse(readFileSync(BUNDLE, 'utf-8')); - - const result = readAndUpdate(tmpPath, ({ frontmatter, body }) => { - frontmatter.revision = (frontmatter.revision || 0) + 1; - frontmatter.source_annotations = bundle.annotations; - frontmatter.annotation_digest = computeAnnotationDigest(bundle.annotations); - return { frontmatter, body }; - }); - assert.equal(result.valid, true, `readAndUpdate must return valid: ${JSON.stringify(result.errors || [])}`); - - const parsed = parseDocument(readFileSync(tmpPath, 'utf-8')); - assert.equal(parsed.valid, true, `re-parsed file must be valid: ${JSON.stringify(parsed.errors || [])}`); - const fm = parsed.parsed.frontmatter; - assert.equal(fm.revision, 1, 'revision must be 1 after first round-trip'); - assert.equal(Array.isArray(fm.source_annotations), true, 'source_annotations must be array'); - assert.equal(fm.source_annotations.length, 2, 'source_annotations must have 2 entries from bundle fixture'); - assert.match(fm.annotation_digest, /^[0-9a-f]{16}$/, 'annotation_digest must be 16-hex-char SHA-256 prefix'); - } finally { - rmSync(tmpDir, { recursive: true, force: true }); - } -}); diff --git a/plugins/voyage/tests/integration/annotation-roundtrip.test.mjs b/plugins/voyage/tests/integration/annotation-roundtrip.test.mjs deleted file mode 100644 index d6f820e..0000000 --- a/plugins/voyage/tests/integration/annotation-roundtrip.test.mjs +++ /dev/null @@ -1,133 +0,0 @@ -// tests/integration/annotation-roundtrip.test.mjs -// SC2 + SC3 + SC7 integration tests for the annotation round-trip pipeline. -// -// SC2 (byte-identical empty round-trip): -// For each target fixture (brief/plan/review), assert that -// stripAnchors(addAnchors(body, [])) === body, byte-for-byte. -// -// SC3 (scale: >=50 steps + >=100 anchors): -// On the 51-step scale fixture, generate 100 anchors above varied lines, -// run addAnchors -> stripAnchors, assert the original body is restored -// byte-for-byte. -// -// SC7 (per-target isolation): -// parseAnchors(stripAnchors(addAnchors(body, anchors))) === [] — once -// anchors are stripped, no residual voyage:anchor markers remain that -// parseAnchors would re-detect. - -import { test } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { readFileSync } from 'node:fs'; -import { join, dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { parseDocument } from '../../lib/util/frontmatter.mjs'; -import { parseAnchors, addAnchors, stripAnchors } from '../../lib/parsers/anchor-parser.mjs'; - -const HERE = dirname(fileURLToPath(import.meta.url)); -const ROOT = join(HERE, '..', '..'); -const FIX_DIR = join(ROOT, 'tests/fixtures/annotation'); - -function readBody(fixture) { - const text = readFileSync(join(FIX_DIR, fixture), 'utf-8'); - const doc = parseDocument(text); - assert.ok(doc.valid, `fixture ${fixture} did not parse: ${(doc.errors || []).map(e => e.message).join(', ')}`); - return doc.parsed.body; -} - -test('annotation-brief.md byte-identical empty round-trip (SC2)', () => { - const body = readBody('annotation-brief.md'); - const out = stripAnchors(addAnchors(body, [])); - assert.strictEqual(out, body, 'empty addAnchors+stripAnchors must be byte-identical'); -}); - -test('annotation-plan.md byte-identical empty round-trip (SC2)', () => { - const body = readBody('annotation-plan.md'); - const out = stripAnchors(addAnchors(body, [])); - assert.strictEqual(out, body, 'empty addAnchors+stripAnchors must be byte-identical'); -}); - -test('annotation-review.md byte-identical empty round-trip (SC2)', () => { - const body = readBody('annotation-review.md'); - const out = stripAnchors(addAnchors(body, [])); - assert.strictEqual(out, body, 'empty addAnchors+stripAnchors must be byte-identical'); -}); - -test('annotation-plan-large.md scale (51 steps + 100 anchors) round-trip (SC3)', () => { - const body = readBody('annotation-plan-large.md'); - const lineCount = body.split('\n').length; - // Generate 100 anchors targeting safe paragraph lines. Place them above - // line numbers that are deliberately avoided by anchor-parser placement - // rules: skip anchor insertion above headings and inside fenced blocks. - // Strategy: pick 100 safe insertion points by walking blank lines outside - // fenced blocks; anchor at line N inserts above line N (so line N must - // be a content line, not a fence delimiter). - const lines = body.split('\n'); - const safe = []; - let inFence = false; - for (let i = 0; i < lines.length; i++) { - const ln = lines[i]; - if (/^```/.test(ln)) { inFence = !inFence; continue; } - if (inFence) continue; - // Skip headings, blank lines, list items, and structural anchors - if (ln.startsWith('#') || ln.trim() === '' || /^\s*[-*+]\s/.test(ln)) continue; - safe.push(i + 1); // 1-indexed line number - } - assert.ok(safe.length >= 100, `need >=100 safe insertion points; got ${safe.length}`); - const anchors = []; - for (let n = 0; n < 100; n++) { - anchors.push({ - id: `ANN-${String(n + 1).padStart(4, '0')}`, - target: `step-${(n % 51) + 1}`, - line: safe[n], - intent: ['fix', 'change', 'question', 'block'][n % 4], - }); - } - const annotated = addAnchors(body, anchors); - // sanity: 100 anchors produced - const parsed = parseAnchors(annotated); - assert.ok(parsed.valid, `parseAnchors on annotated body failed: ${(parsed.errors || []).map(e => e.message).join('; ')}`); - assert.strictEqual(parsed.parsed.length, 100, `expected 100 anchors after addAnchors, got ${parsed.parsed.length}`); - // Round-trip restores body byte-for-byte. - const restored = stripAnchors(annotated); - assert.strictEqual(restored, body, 'addAnchors -> stripAnchors must round-trip byte-identical at scale'); -}); - -test('parseAnchors(stripAnchors(addAnchors(brief, anchors))) === [] (SC7 brief)', () => { - const body = readBody('annotation-brief.md'); - const lines = body.split('\n'); - // Pick a content line — first non-blank, non-heading line - const target = lines.findIndex(l => l.length > 0 && !l.startsWith('#')) + 1; - assert.ok(target > 0, 'brief fixture has no content lines'); - const anchors = [{ id: 'ANN-0001', target: 'intent', line: target, intent: 'change' }]; - const annotated = addAnchors(body, anchors); - const stripped = stripAnchors(annotated); - const result = parseAnchors(stripped); - assert.ok(result.valid, 'parseAnchors on stripped body should be valid'); - assert.deepStrictEqual(result.parsed, [], 'no anchors should remain after stripAnchors'); -}); - -test('parseAnchors(stripAnchors(addAnchors(plan, anchors))) === [] (SC7 plan)', () => { - const body = readBody('annotation-plan.md'); - const lines = body.split('\n'); - const target = lines.findIndex(l => l.startsWith('A minimal')) + 1; - assert.ok(target > 0, 'plan fixture missing expected content line'); - const anchors = [{ id: 'ANN-0001', target: 'context', line: target, intent: 'fix' }]; - const annotated = addAnchors(body, anchors); - const stripped = stripAnchors(annotated); - const result = parseAnchors(stripped); - assert.ok(result.valid); - assert.deepStrictEqual(result.parsed, []); -}); - -test('parseAnchors(stripAnchors(addAnchors(review, anchors))) === [] (SC7 review)', () => { - const body = readBody('annotation-review.md'); - const lines = body.split('\n'); - const target = lines.findIndex(l => l.startsWith('Verdict')) + 1; - assert.ok(target > 0, 'review fixture missing Verdict line'); - const anchors = [{ id: 'ANN-0001', target: 'executive-summary', line: target, intent: 'question' }]; - const annotated = addAnchors(body, anchors); - const stripped = stripAnchors(annotated); - const result = parseAnchors(stripped); - assert.ok(result.valid); - assert.deepStrictEqual(result.parsed, []); -}); diff --git a/plugins/voyage/tests/integration/schema-rollback.test.mjs b/plugins/voyage/tests/integration/schema-rollback.test.mjs deleted file mode 100644 index eb9b514..0000000 --- a/plugins/voyage/tests/integration/schema-rollback.test.mjs +++ /dev/null @@ -1,135 +0,0 @@ -// tests/integration/schema-rollback.test.mjs -// SC5b: post-write validator failure rolls back byte-identical pre-revision state. -// -// Exercises lib/util/revision-guard.mjs revisionGuard(): -// - Apply a deliberately-corrupting mutator that produces an artifact -// the validator will reject (missing required section / wrong type). -// - Assert outcome === 'rolled-back'. -// - Assert sha256_after === sha256_before (byte-identical recovery). -// - Assert .local.bak is deleted on the rollback path. -// -// Cases: -// 1. brief-rollback — strip a required body section -// 2. plan-rollback — break plan structure (rename Implementation Plan) -// 3. review-rollback — flip type to non-trekreview -// 4. sha256-invariance-cross-target — across all three targets, verify -// the byte-invariance holds for at least one common corrupting class -// (frontmatter `type:` flip). - -import { test } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { copyFileSync, existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join, dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { createHash } from 'node:crypto'; -import { revisionGuard } from '../../lib/util/revision-guard.mjs'; -import { validateBrief } from '../../lib/validators/brief-validator.mjs'; -import { validatePlan } from '../../lib/validators/plan-validator.mjs'; -import { validateReview } from '../../lib/validators/review-validator.mjs'; - -const HERE = dirname(fileURLToPath(import.meta.url)); -const ROOT = join(HERE, '..', '..'); -const FIX_DIR = join(ROOT, 'tests/fixtures/annotation'); - -function sha256(p) { - return createHash('sha256').update(readFileSync(p)).digest('hex'); -} - -function tmpCopy(name) { - const dir = mkdtempSync(join(tmpdir(), 'voyage-rollback-')); - const dst = join(dir, name); - copyFileSync(join(FIX_DIR, name), dst); - return { dir, path: dst }; -} - -test('brief-rollback: strip Goal section -> validator FAIL -> byte-identical restore', () => { - const { dir, path } = tmpCopy('annotation-brief.md'); - try { - const sha_before = sha256(path); - const result = revisionGuard( - path, - ({ frontmatter, body }) => ({ - frontmatter, - body: body.replace(/## Goal[\s\S]*?(?=\n## Success Criteria)/, ''), // strip Goal section - }), - validateBrief, - ); - assert.strictEqual(result.outcome, 'rolled-back', `expected rolled-back, got ${result.outcome}`); - const sha_after = sha256(path); - assert.strictEqual(sha_after, sha_before, 'sha256 must be byte-identical after rollback'); - assert.ok(!existsSync(path + '.local.bak'), '.local.bak must be deleted after rollback'); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); - -test('plan-rollback: rename Implementation Plan heading -> validator FAIL -> byte-identical restore', () => { - const { dir, path } = tmpCopy('annotation-plan.md'); - try { - const sha_before = sha256(path); - const result = revisionGuard( - path, - ({ frontmatter, body }) => ({ - frontmatter, - // Inject a forbidden phase-style heading the plan-schema rejects (PLAN_FORBIDDEN_HEADING) - body: body + '\n\n### Fase 99: This forbidden heading triggers PLAN_FORBIDDEN_HEADING\n', - }), - validatePlan, - ); - assert.strictEqual(result.outcome, 'rolled-back', `expected rolled-back, got ${result.outcome}`); - const sha_after = sha256(path); - assert.strictEqual(sha_after, sha_before, 'sha256 must be byte-identical after rollback'); - assert.ok(!existsSync(path + '.local.bak'), '.local.bak must be deleted after rollback'); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); - -test('review-rollback: flip type to non-trekreview -> validator FAIL -> byte-identical restore', () => { - const { dir, path } = tmpCopy('annotation-review.md'); - try { - const sha_before = sha256(path); - const result = revisionGuard( - path, - ({ frontmatter, body }) => ({ - frontmatter: { ...frontmatter, type: 'not-a-real-type' }, - body, - }), - validateReview, - ); - assert.strictEqual(result.outcome, 'rolled-back', `expected rolled-back, got ${result.outcome}`); - const sha_after = sha256(path); - assert.strictEqual(sha_after, sha_before, 'sha256 must be byte-identical after rollback'); - assert.ok(!existsSync(path + '.local.bak'), '.local.bak must be deleted after rollback'); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); - -test('sha256-invariance-cross-target: byte-identical rollback for all three targets', () => { - const cases = [ - { fixture: 'annotation-brief.md', validator: validateBrief, frontmatterCorruption: { type: 'wrong-type' } }, - { fixture: 'annotation-plan.md', validator: validatePlan, bodyCorruption: '\n\n### Fase 1: forbidden\n' }, - { fixture: 'annotation-review.md', validator: validateReview, frontmatterCorruption: { findings: 'not-an-array' } }, - ]; - for (const c of cases) { - const { dir, path } = tmpCopy(c.fixture); - try { - const sha_before = sha256(path); - const result = revisionGuard( - path, - ({ frontmatter, body }) => ({ - frontmatter: c.frontmatterCorruption ? { ...frontmatter, ...c.frontmatterCorruption } : frontmatter, - body: c.bodyCorruption ? body + c.bodyCorruption : body, - }), - c.validator, - ); - assert.strictEqual(result.outcome, 'rolled-back', `${c.fixture}: expected rolled-back, got ${result.outcome}`); - assert.strictEqual(sha256(path), sha_before, `${c.fixture}: sha256 must be byte-identical after rollback`); - assert.ok(!existsSync(path + '.local.bak'), `${c.fixture}: .local.bak must be deleted after rollback`); - } finally { - rmSync(dir, { recursive: true, force: true }); - } - } -}); diff --git a/plugins/voyage/tests/lib/doc-consistency.test.mjs b/plugins/voyage/tests/lib/doc-consistency.test.mjs index 686805e..debb032 100644 --- a/plugins/voyage/tests/lib/doc-consistency.test.mjs +++ b/plugins/voyage/tests/lib/doc-consistency.test.mjs @@ -80,7 +80,7 @@ test('commands/trekexecute.md still parses v1.7 plan schema', () => { test('settings.json has only known top-level scopes after Spor 0 cleanup', () => { const cfg = JSON.parse(read('settings.json')); - const known = ['trekplan', 'trekresearch', 'trekrevise']; + const known = ['trekplan', 'trekresearch']; for (const k of Object.keys(cfg)) { assert.ok(known.includes(k), `Unknown top-level scope in settings.json: ${k}`); } @@ -94,10 +94,9 @@ test('settings.json no longer carries vestigial exploration block', () => { 'agentTeam block was vestigial — should be deleted in v3.1.0 Spor 0'); }); -test('CLAUDE.md mentions all seven pipeline commands', () => { +test('CLAUDE.md mentions all six pipeline commands', () => { // v4.1 Step 21 — added /trekcontinue to coverage (was 5/6 before). - // v4.2 Step 12 — added /trekrevise (Handover 8 producer), bringing the - // canonical pipeline to seven commands. + // v5.0.0 — /trekrevise removed (bespoke playground retired); back to six. const md = read('CLAUDE.md'); for (const c of [ '/trekbrief', @@ -105,7 +104,6 @@ test('CLAUDE.md mentions all seven pipeline commands', () => { '/trekplan', '/trekexecute', '/trekreview', - '/trekrevise', '/trekcontinue', ]) { assert.ok(md.includes(c), `CLAUDE.md missing reference to ${c}`); @@ -261,7 +259,6 @@ const PIPELINE_COMMANDS = [ 'trekplan.md', 'trekexecute.md', 'trekreview.md', - 'trekrevise.md', 'trekcontinue.md', ]; @@ -403,246 +400,87 @@ test('commands/trekplan.md Phase 8 seals Opus-4.7 schema-drift defense', () => { ); }); -// --- v4.2 Step 12 — Handover 8 + annotation pipeline pins --- +// --- v5.0.0 — bespoke playground + /trekrevise + Handover 8 removed --- // -// CLAUDE.md / README.md / CHANGELOG / annotation-quickstart pins are deferred -// to Step 13 (post-write of those files). Step 12 only pins HANDOVER-CONTRACTS, -// templates, scaffold-files, and the parseAnchors round-trip on the example -// fixture. +// The v4.2/v4.3 bespoke playground SPA, the /trekrevise command, and +// Handover 8 (annotation → revision) were removed in v5.0.0. Producing +// commands now render artifacts to self-contained HTML via +// scripts/render-artifact.mjs and direct operators at the official +// `/playground` plugin for annotation. These pins lock the removal in. -import { existsSync, statSync } from 'node:fs'; +import { existsSync } from 'node:fs'; -test('HANDOVER-CONTRACTS.md contains Handover 8 section (annotation → revision)', () => { +test('playground/ directory no longer exists (removed in v5.0.0)', () => { + assert.ok( + !existsSync(join(ROOT, 'playground')), + 'plugins/voyage/playground/ should be deleted — the bespoke playground was retired in v5.0.0', + ); +}); + +test('commands/trekrevise.md no longer exists (removed in v5.0.0)', () => { + assert.ok( + !existsSync(join(ROOT, 'commands/trekrevise.md')), + '/trekrevise was removed in v5.0.0 — its command file should be gone', + ); +}); + +test('Handover 8 deleted from HANDOVER-CONTRACTS.md (back to seven handovers)', () => { const text = read('docs/HANDOVER-CONTRACTS.md'); - assert.ok( - text.includes('## Handover 8'), - 'docs/HANDOVER-CONTRACTS.md should document Handover 8 (annotation → revision) — added in v4.2', - ); + assert.ok(!text.includes('## Handover 8'), 'Handover 8 section should be removed in v5.0.0'); + assert.ok(text.includes('## Handover 7'), 'Handover 7 must remain'); }); -test('HANDOVER-CONTRACTS.md Handover 8 names annotation_digest and source_annotations', () => { - const text = read('docs/HANDOVER-CONTRACTS.md'); - const h8Start = text.indexOf('## Handover 8'); - assert.ok(h8Start >= 0, 'Handover 8 heading missing'); - const h8End = text.indexOf('## Stability summary', h8Start); - assert.ok(h8End > h8Start, 'Stability summary heading missing — could not bound Handover 8'); - const h8 = text.slice(h8Start, h8End); - assert.ok( - h8.includes('annotation_digest'), - 'Handover 8 section should document the annotation_digest frontmatter field', - ); - assert.ok( - h8.includes('source_annotations'), - 'Handover 8 section should document the source_annotations frontmatter field', - ); - assert.ok( - h8.includes('revision'), - 'Handover 8 section should document the revision counter field', - ); -}); - -test('templates/plan-template.md documents annotation revision fields', () => { - const tpl = read('templates/plan-template.md'); - assert.ok( - tpl.includes('revision:'), - 'plan-template.md must document optional revision counter (Handover 8)', - ); - assert.ok( - tpl.includes('source_annotations:'), - 'plan-template.md must document optional source_annotations list (Handover 8)', - ); - assert.ok( - tpl.includes('annotation_digest'), - 'plan-template.md must document optional annotation_digest field (Handover 8)', - ); -}); - -test('templates/trekbrief-template.md documents annotation revision fields', () => { - const tpl = read('templates/trekbrief-template.md'); - assert.ok( - tpl.includes('revision:'), - 'trekbrief-template.md must document optional revision counter (Handover 8)', - ); - assert.ok( - tpl.includes('source_annotations:'), - 'trekbrief-template.md must document optional source_annotations list (Handover 8)', - ); - assert.ok( - tpl.includes('annotation_digest'), - 'trekbrief-template.md must document optional annotation_digest field (Handover 8)', - ); -}); - -test('templates/trekreview-template.md documents annotation revision fields', () => { - const tpl = read('templates/trekreview-template.md'); - assert.ok( - tpl.includes('revision:'), - 'trekreview-template.md must document optional revision counter (Handover 8)', - ); - assert.ok( - tpl.includes('source_annotations:'), - 'trekreview-template.md must document optional source_annotations list (Handover 8)', - ); - assert.ok( - tpl.includes('annotation_digest'), - 'trekreview-template.md must document optional annotation_digest field (Handover 8)', - ); -}); - -test('playground/ directory exists at voyage root (Handover 8 producer surface)', () => { - const playgroundDir = join(ROOT, 'playground'); - assert.ok(existsSync(playgroundDir), 'playground/ directory missing'); - assert.ok(statSync(playgroundDir).isDirectory(), 'playground/ is not a directory'); - // Self-contained HTML must exist - assert.ok( - existsSync(join(playgroundDir, 'voyage-playground.html')), - 'playground/voyage-playground.html missing — operator-facing entry point', - ); -}); - -test('playground/ files do NOT import or reference `marked` (risk-assessor H1)', () => { - // Walk playground/ recursively. Exclude vendor/playground-design-system - // (consumed via the shared design system; not part of voyage's playground - // markdown renderer). Exclude any *MANIFEST.json files. Assert no file - // contains the standalone identifier `marked` (case-sensitive, word-boundary). - // markdown-it is the locked renderer per research-03 + alternatives table. - const playgroundDir = join(ROOT, 'playground'); - assert.ok(existsSync(playgroundDir), 'playground/ directory missing — cannot verify marked-ban'); - const offenders = []; - function walk(dir) { - for (const entry of readdirSync(dir)) { - const p = join(dir, entry); - const s = statSync(p); - if (s.isDirectory()) { - // Skip vendor design-system trees (shared infra, not voyage's renderer) - if (entry === 'playground-design-system') continue; - walk(p); - } else if (s.isFile()) { - // Skip vendor manifest JSONs - if (entry.endsWith('MANIFEST.json')) continue; - if (entry === 'VENDOR-MANIFEST.json') continue; - const txt = readFileSync(p, 'utf-8'); - if (/\bmarked\b/.test(txt)) { - offenders.push(p.slice(ROOT.length + 1)); - } - } - } - } - walk(playgroundDir); - assert.deepStrictEqual( - offenders, - [], - `playground/ files contain banned identifier "marked": ${offenders.join(', ')}. ` + - `Use markdown-it instead — see plan Alternatives table (Issue #3515 disqualifies marked).`, - ); -}); - -test('scripts/render-artifact.mjs exists (SC1/SC11 self-render gate)', () => { +test('scripts/render-artifact.mjs exists (v5.0.0 render-and-link step)', () => { assert.ok( existsSync(join(ROOT, 'scripts/render-artifact.mjs')), - 'scripts/render-artifact.mjs missing — required by SC1 (offline render) and SC11 (pipeline-self-eat)', + 'scripts/render-artifact.mjs is required — producing commands call it to render artifacts to HTML', ); }); -test('lib/util/revision-guard.mjs exists (plan-critic M4 — atomic-write rollback guard)', () => { - assert.ok( - existsSync(join(ROOT, 'lib/util/revision-guard.mjs')), - 'lib/util/revision-guard.mjs missing — required for /trekrevise rollback hygiene', - ); -}); - -test('tests/fixtures/annotation/annotation-example.md parses cleanly via parseAnchors (ESM)', async () => { - // Plan-critic m4 — fix the SC12 require/import mixup. Use ESM dynamic import, - // not require(). The parser is pure — no I/O, no side effects. - const { parseAnchors } = await import('../../lib/parsers/anchor-parser.mjs'); - const fixturePath = join(ROOT, 'tests/fixtures/annotation/annotation-example.md'); - assert.ok(existsSync(fixturePath), 'tests/fixtures/annotation/annotation-example.md missing'); - const result = parseAnchors(readFileSync(fixturePath, 'utf-8')); - assert.ok( - result.valid, - `parseAnchors failed on annotation-example.md fixture: ${JSON.stringify(result.errors || [])}`, - ); -}); - -// --- v4.2 Step 13 — late doc-consistency pins (post-write of CLAUDE / READMEs / CHANGELOG / quickstart) --- -// -// These were deferred from Step 12 per plan-critic M1 ordering finding — -// Step 13 is where these files are written, so pins go here. - -test('plugin README.md mentions /trekrevise in commands section', () => { - // Already covered for CLAUDE.md by the "all seven pipeline commands" test; - // this pin extends coverage to the plugin-level README. - const md = read('README.md'); - assert.ok( - md.includes('/trekrevise'), - 'plugin README.md must reference /trekrevise (added in v4.2 Step 13)', - ); -}); - -test('marketplace root README.md mentions /trekrevise and v4.2.0', () => { - // ../../README.md is the marketplace landing — must surface v4.2 ship. - // Path traversal is allowed here per feedback_plugin_scope_strict - // (root README updates are explicitly in Step 13's scope). - const md = read('../../README.md'); - assert.ok( - md.includes('/trekrevise') || md.includes('trekrevise'), - 'marketplace root README.md must reference /trekrevise (v4.2)', - ); - assert.ok( - md.includes('v4.2.0'), - 'marketplace root README.md must reference voyage v4.2.0', - ); -}); - -test('CHANGELOG.md has v4.2.0 entry', () => { - const cl = read('CHANGELOG.md'); - assert.match( - cl, - /## v4\.2\.0\b/, - 'CHANGELOG.md must include "## v4.2.0" entry per Keep-a-Changelog 1.1.0', - ); -}); - -test('docs/annotation-quickstart.md exists with ≤7 numbered steps and example-fixture reference', () => { - // SC12 — operator-facing quickstart. The plan caps numbered steps at 7 - // to keep cognitive load minimal; reference to the example fixture - // anchors the doc to a concrete artifact operators can replay. - const path = 'docs/annotation-quickstart.md'; - assert.ok(existsSync(join(ROOT, path)), `${path} missing`); - const text = read(path); - // Numbered top-level steps: lines starting with "1." through "7." at - // line-start. Forbid 8.+ line-starts. - const numberedSteps = (text.match(/^[1-9]\./gm) || []); - for (const s of numberedSteps) { - const n = parseInt(s, 10); +test('producing commands reference render-artifact.mjs (render-and-link step)', () => { + for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) { assert.ok( - n >= 1 && n <= 7, - `${path} contains step ${s} — only 1.-7. permitted (single-screen quickstart)`, + read(`commands/${f}`).includes('render-artifact.mjs'), + `commands/${f} must wire the render-artifact.mjs render-and-link step (v5.0.0)`, ); } - assert.ok( - text.includes('tests/fixtures/annotation/annotation-example.md'), - `${path} must reference the canonical example fixture for hands-on verification`, - ); }); -test('commands/trekplan.md Phase 9 documents plan_critic injection via readAndUpdate (906f155d)', () => { - // Phase 9 (adversarial review) writes the plan-critic verdict back into - // plan.md frontmatter AFTER plan-review-dedup completes. The inject must - // happen post-Phase-8 (write) because Phase 8 precedes Phase 9 in the - // pipeline — the value cannot be in Phase 8's frontmatter template. - // Both the field name (plan_critic) and the inject mechanism - // (readAndUpdate from lib/util/markdown-write.mjs) must be documented - // so future maintainers can trace the contract. - const text = read('commands/trekplan.md'); - assert.match( - text, - /plan_critic/, - 'commands/trekplan.md must document plan_critic frontmatter field (906f155d)', - ); - assert.match( - text, - /readAndUpdate/, - 'commands/trekplan.md must reference readAndUpdate from lib/util/markdown-write.mjs (906f155d)', - ); +test('producing commands point operators at the /playground plugin for annotation', () => { + for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) { + assert.ok( + read(`commands/${f}`).includes('/playground'), + `commands/${f} must mention the /playground plugin as the annotation path (v5.0.0)`, + ); + } +}); + +test('CHANGELOG.md has v5.0.0 entry', () => { + const cl = read('CHANGELOG.md'); + assert.match(cl, /## v5\.0\.0\b/, 'CHANGELOG.md must include "## v5.0.0" entry'); +}); + +test('CHANGELOG.md retains v4.2.0 entry (history is not rewritten)', () => { + const cl = read('CHANGELOG.md'); + assert.match(cl, /## v4\.2\.0\b/, 'CHANGELOG.md must keep the historical "## v4.2.0" entry'); +}); + +test('operational files no longer reference trekrevise (v5.0.0 removal)', () => { + // Templates, the touched command/orchestrator files, settings.json, and the + // handover-contracts doc must be fully scrubbed. CLAUDE.md / README.md are + // intentionally allowed to mention /trekrevise in their "removed in v5.0.0" + // prose — those are historical notes, not live references. + const targets = [ + 'settings.json', + 'docs/HANDOVER-CONTRACTS.md', + 'templates/plan-template.md', 'templates/trekbrief-template.md', 'templates/trekreview-template.md', + 'commands/trekplan.md', 'commands/trekbrief.md', 'commands/trekreview.md', + 'agents/planning-orchestrator.md', + ]; + for (const t of targets) { + assert.ok( + !read(t).includes('trekrevise'), + `${t} still references trekrevise — it was removed in v5.0.0`, + ); + } }); diff --git a/plugins/voyage/tests/lib/markdown-write.test.mjs b/plugins/voyage/tests/lib/markdown-write.test.mjs deleted file mode 100644 index f7d06d7..0000000 --- a/plugins/voyage/tests/lib/markdown-write.test.mjs +++ /dev/null @@ -1,189 +0,0 @@ -// tests/lib/markdown-write.test.mjs -// Unit tests for lib/util/markdown-write.mjs (v4.2) - -import { test } from 'node:test'; -import assert from 'node:assert/strict'; -import { mkdtempSync, rmSync, readFileSync, existsSync, writeFileSync, readdirSync, statSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join, resolve, dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { - serializeFrontmatter, - atomicWriteMarkdown, - readAndUpdate, -} from '../../lib/util/markdown-write.mjs'; -import { parseFrontmatter, parseDocument } from '../../lib/util/frontmatter.mjs'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const FIXTURES_ROOT = resolve(__dirname, '..', 'fixtures'); - -test('serializeFrontmatter — empty object returns empty string', () => { - assert.equal(serializeFrontmatter({}), ''); -}); - -test('serializeFrontmatter — round-trip fidelity for scalars + arrays + list-of-dicts', () => { - const obj = { - name: 'voyage-test', - revision: 0, - enabled: true, - notes: null, - tags: ['alpha', 'beta', 'gamma'], - findings: [ - { id: 'a', severity: 'major' }, - { id: 'b', severity: 'minor' }, - ], - }; - const yaml = serializeFrontmatter(obj); - const reparsed = parseFrontmatter(yaml).parsed; - assert.deepEqual(reparsed, obj); -}); - -test('serializeFrontmatter — block-style YAML for arrays (no flow style)', () => { - const yaml = serializeFrontmatter({ tags: ['a', 'b'] }); - assert.ok(!yaml.includes('[a, b]'), 'flow-style array forbidden'); - assert.ok(yaml.includes('tags:\n - a\n - b'), 'block-style required'); -}); - -test('serializeFrontmatter — strings with colons are quoted', () => { - const yaml = serializeFrontmatter({ task: 'Re-architect: phase 2' }); - assert.match(yaml, /task: ".*Re-architect.*phase 2.*"/); - const reparsed = parseFrontmatter(yaml).parsed; - assert.equal(reparsed.task, 'Re-architect: phase 2'); -}); - -test('serializeFrontmatter — integer revision: 0 emitted unquoted', () => { - const yaml = serializeFrontmatter({ revision: 0 }); - assert.equal(yaml, 'revision: 0'); -}); - -test('serializeFrontmatter — round-trips 6-key source_annotations dict (v4.2 schema)', () => { - const obj = { - revision: 1, - source_annotations: [ - { - id: 'ANN-0001', - target_artifact: 'plan.md', - target_anchor: 'step-3', - intent: 'change', - comment: 'Reorder before step 4', - timestamp: '2026-05-09T10:00:00Z', - }, - { - id: 'ANN-0002', - target_artifact: 'plan.md', - target_anchor: 'step-7', - intent: 'fix', - comment: 'typo in heading', - timestamp: '2026-05-09T10:05:00Z', - }, - ], - annotation_digest: 'abc123def4567890', - }; - const yaml = serializeFrontmatter(obj); - const reparsed = parseFrontmatter(yaml).parsed; - assert.deepEqual(reparsed, obj, '6-key list-of-dict must round-trip'); -}); - -test('atomicWriteMarkdown — writes file with frontmatter + body', () => { - const dir = mkdtempSync(join(tmpdir(), 'mdw-test-')); - try { - const path = join(dir, 'plan.md'); - atomicWriteMarkdown(path, { plan_version: '1.7', revision: 0 }, '# Title\n\nBody.\n'); - const text = readFileSync(path, 'utf-8'); - assert.match(text, /^---\nplan_version: "?1\.7"?\nrevision: 0\n---\n# Title\n\nBody\.\n$/); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); - -test('atomicWriteMarkdown — leaves no .tmp orphan after success', () => { - const dir = mkdtempSync(join(tmpdir(), 'mdw-test-')); - try { - const path = join(dir, 'plan.md'); - atomicWriteMarkdown(path, { ok: true }, 'body'); - assert.ok(existsSync(path)); - assert.ok(!existsSync(path + '.tmp')); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); - -test('atomicWriteMarkdown — overwrites existing file atomically', () => { - const dir = mkdtempSync(join(tmpdir(), 'mdw-test-')); - try { - const path = join(dir, 'plan.md'); - writeFileSync(path, 'old content'); - atomicWriteMarkdown(path, { new: true }, 'new body\n'); - const text = readFileSync(path, 'utf-8'); - assert.match(text, /new: true/); - assert.match(text, /new body/); - assert.ok(!text.includes('old content')); - assert.ok(!existsSync(path + '.tmp')); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); - -test('atomicWriteMarkdown — preserves body bytes verbatim', () => { - const dir = mkdtempSync(join(tmpdir(), 'mdw-test-')); - try { - const path = join(dir, 'plan.md'); - const body = '# Title\n\n- item with `code`\n\n```yaml\nmanifest:\n expected_paths:\n - foo\n```\n\nTrailing text.'; - atomicWriteMarkdown(path, { v: 1 }, body); - const text = readFileSync(path, 'utf-8'); - const split = text.split('---\n'); - const recoveredBody = split.slice(2).join('---\n'); - assert.equal(recoveredBody, body); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); - -test('readAndUpdate — round-trips frontmatter + body via mutator', () => { - const dir = mkdtempSync(join(tmpdir(), 'mdw-test-')); - try { - const path = join(dir, 'plan.md'); - atomicWriteMarkdown(path, { plan_version: '1.7', revision: 0 }, '# Original\nBody.\n'); - const result = readAndUpdate(path, ({ frontmatter, body }) => ({ - frontmatter: { ...frontmatter, revision: 1 }, - body, - })); - assert.equal(result.valid, true); - const re = parseDocument(readFileSync(path, 'utf-8')); - assert.equal(re.parsed.frontmatter.revision, 1); - assert.match(re.parsed.body, /# Original/); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); - -// Round-trip ALL existing fixture frontmatters (per risk-assessor C3). -// Walk tests/fixtures/**, parse + serialize + parse, assert deep-equal. -function walkMd(root, out = []) { - if (!existsSync(root)) return out; - for (const entry of readdirSync(root)) { - const p = join(root, entry); - const st = statSync(p); - if (st.isDirectory()) walkMd(p, out); - else if (entry.endsWith('.md')) out.push(p); - } - return out; -} - -test('serializeFrontmatter — round-trips ALL existing fixture frontmatters', () => { - const fixtures = walkMd(FIXTURES_ROOT); - let checked = 0; - for (const path of fixtures) { - const text = readFileSync(path, 'utf-8'); - const parsed = parseDocument(text); - if (!parsed.valid) continue; // some fixtures are intentionally malformed - const fm = parsed.parsed.frontmatter; - if (!fm || Object.keys(fm).length === 0) continue; - const yaml = serializeFrontmatter(fm); - const reparsed = parseFrontmatter(yaml); - if (!reparsed.valid) continue; // skip malformed-on-purpose fixtures - assert.deepEqual(reparsed.parsed, fm, `round-trip failed for fixture: ${path}`); - checked++; - } - assert.ok(checked > 0, 'expected to round-trip at least one fixture'); -}); diff --git a/plugins/voyage/tests/lib/revision-guard.test.mjs b/plugins/voyage/tests/lib/revision-guard.test.mjs deleted file mode 100644 index 3e7e4b0..0000000 --- a/plugins/voyage/tests/lib/revision-guard.test.mjs +++ /dev/null @@ -1,135 +0,0 @@ -// tests/lib/revision-guard.test.mjs -// Unit tests for lib/util/revision-guard.mjs (v4.2) - -import { test } from 'node:test'; -import assert from 'node:assert/strict'; -import { mkdtempSync, rmSync, readFileSync, existsSync, writeFileSync, copyFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { createHash } from 'node:crypto'; -import { revisionGuard } from '../../lib/util/revision-guard.mjs'; -import { atomicWriteMarkdown } from '../../lib/util/markdown-write.mjs'; - -function sha256(path) { - return createHash('sha256').update(readFileSync(path)).digest('hex'); -} - -const ALWAYS_VALID = () => ({ valid: true, errors: [], warnings: [] }); -const ALWAYS_INVALID = () => ({ valid: false, errors: [{ code: 'TEST', message: 'forced fail' }], warnings: [] }); - -test('revisionGuard — validator-PASS commits revision and deletes bak', () => { - const dir = mkdtempSync(join(tmpdir(), 'rg-test-')); - try { - const path = join(dir, 'plan.md'); - atomicWriteMarkdown(path, { plan_version: '1.7', revision: 0 }, '# Hello\n'); - const r = revisionGuard( - path, - ({ frontmatter, body }) => ({ frontmatter: { ...frontmatter, revision: 1 }, body }), - ALWAYS_VALID, - ); - assert.equal(r.outcome, 'applied'); - assert.ok(!existsSync(path + '.local.bak'), 'bak should be deleted on success'); - const text = readFileSync(path, 'utf-8'); - assert.match(text, /revision: 1/); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); - -test('revisionGuard — validator-FAIL rolls back to byte-identical pre-revision', () => { - const dir = mkdtempSync(join(tmpdir(), 'rg-test-')); - try { - const path = join(dir, 'plan.md'); - atomicWriteMarkdown(path, { plan_version: '1.7', revision: 0 }, '# Hello\n'); - const before = sha256(path); - const r = revisionGuard( - path, - ({ frontmatter, body }) => ({ frontmatter: { ...frontmatter, revision: 1 }, body }), - ALWAYS_INVALID, - ); - assert.equal(r.outcome, 'rolled-back'); - const after = sha256(path); - assert.equal(after, before, 'rollback must restore byte-identical content'); - assert.ok(!existsSync(path + '.local.bak'), 'bak should be cleaned up after rollback'); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); - -test('revisionGuard — pre-existing .local.bak aborts with operator guidance', () => { - const dir = mkdtempSync(join(tmpdir(), 'rg-test-')); - try { - const path = join(dir, 'plan.md'); - atomicWriteMarkdown(path, { plan_version: '1.7' }, '# Hello\n'); - const bak = path + '.local.bak'; - writeFileSync(bak, 'stale backup from prior run'); - const r = revisionGuard(path, ({ frontmatter, body }) => ({ frontmatter, body }), ALWAYS_VALID); - assert.equal(r.outcome, 'mutator-failed'); - assert.match(r.error, /pre-existing backup/); - // Original file untouched, stale bak preserved for operator inspection - assert.equal(readFileSync(bak, 'utf-8'), 'stale backup from prior run'); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); - -test('revisionGuard — mutator that throws restores original via bak', () => { - const dir = mkdtempSync(join(tmpdir(), 'rg-test-')); - try { - const path = join(dir, 'plan.md'); - atomicWriteMarkdown(path, { plan_version: '1.7' }, '# Hello\n'); - const before = sha256(path); - const r = revisionGuard( - path, - () => { throw new Error('boom'); }, - ALWAYS_VALID, - ); - assert.equal(r.outcome, 'mutator-failed'); - assert.match(r.error, /boom/); - const after = sha256(path); - assert.equal(after, before, 'mutator-throw must preserve original'); - assert.ok(!existsSync(path + '.local.bak'), 'bak cleaned up after mutator-throw'); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); - -test('revisionGuard — mutator returns invalid object rejected before validator runs', () => { - const dir = mkdtempSync(join(tmpdir(), 'rg-test-')); - try { - const path = join(dir, 'plan.md'); - atomicWriteMarkdown(path, { plan_version: '1.7' }, '# Hello\n'); - const before = sha256(path); - let validatorCalled = false; - const r = revisionGuard( - path, - () => null, // not an object - () => { validatorCalled = true; return { valid: true, errors: [], warnings: [] }; }, - ); - assert.equal(r.outcome, 'mutator-failed'); - assert.equal(validatorCalled, false, 'validator must not run if mutator returned invalid result'); - const after = sha256(path); - assert.equal(after, before, 'invalid mutator must preserve original'); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); - -test('revisionGuard — sha256 fields populated and stable', () => { - const dir = mkdtempSync(join(tmpdir(), 'rg-test-')); - try { - const path = join(dir, 'plan.md'); - atomicWriteMarkdown(path, { plan_version: '1.7', revision: 0 }, '# Hello\n'); - const before = sha256(path); - const r = revisionGuard( - path, - ({ frontmatter, body }) => ({ frontmatter: { ...frontmatter, revision: 1 }, body }), - ALWAYS_VALID, - ); - assert.equal(r.sha256_before, before); - assert.equal(typeof r.sha256_after, 'string'); - assert.notEqual(r.sha256_after, r.sha256_before, 'sha256 must change after applied revision'); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); diff --git a/plugins/voyage/tests/lib/source-annotations.test.mjs b/plugins/voyage/tests/lib/source-annotations.test.mjs deleted file mode 100644 index c2aeb88..0000000 --- a/plugins/voyage/tests/lib/source-annotations.test.mjs +++ /dev/null @@ -1,244 +0,0 @@ -// tests/lib/source-annotations.test.mjs -// Additive-field invariant for source_annotations: array (Handover 8). -// -// Mirrors tests/lib/source-findings.test.mjs:9-13 — the structural three-part -// contract that v4.2 brief-validator + plan-validator + review-validator must -// uphold for the new optional source_annotations frontmatter field: -// -// 1. validators accept an artifact with source_annotations (additive optional) -// 2. frontmatter parser extracts source_annotations as an array -// 3. each entry has the documented annotation shape -// ({id, target_artifact, target_anchor, intent, ...}) -// -// LLM behavior (the planner actually emitting source_annotations) is -// non-testable without live invocation — this test only covers the schema -// half. See Step 12 doc-pin for the operator-level contract. - -import { test } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { parseDocument } from '../../lib/util/frontmatter.mjs'; -import { validateBrief } from '../../lib/validators/brief-validator.mjs'; -import { validatePlan } from '../../lib/validators/plan-validator.mjs'; -import { validateReview } from '../../lib/validators/review-validator.mjs'; - -const ID_RE = /^ANN-\d{4}$/; -const VALID_INTENT = new Set(['fix', 'change', 'question', 'block']); - -function makeFixture(name, body) { - const dir = mkdtempSync(join(tmpdir(), 'voyage-source-ann-')); - const path = join(dir, name); - writeFileSync(path, body); - return { dir, path }; -} - -const BRIEF_WITH_SOURCE_ANNOTATIONS = `--- -type: trekbrief -brief_version: "1.0" -task: Demo brief with source_annotations -slug: source-annotations-demo-brief -research_topics: 0 -research_status: complete -revision: 1 -annotation_digest: deadbeefcafe1234 -source_annotations: - - id: ANN-0001 - target_artifact: brief.md - target_anchor: goal - line: 20 - intent: change - - id: ANN-0002 - target_artifact: brief.md - target_anchor: success-criteria - line: 30 - intent: fix ---- - -# Demo - -## Intent - -Test fixture. - -## Goal - -Test fixture. - -## Success Criteria - -- It validates. -`; - -const PLAN_WITH_SOURCE_ANNOTATIONS = `--- -plan_version: 1.7 -profile: balanced -revision: 2 -annotation_digest: cafebabe98765432 -source_annotations: - - id: ANN-0001 - target_artifact: plan.md - target_anchor: step-1 - line: 25 - intent: fix ---- - -# Demo plan - -## Implementation Plan - -### Step 1: Sentinel - -- **Files:** \`tmp/x.txt\` (new) -- **Changes:** Touch. -- **Verify:** \`test -f tmp/x.txt\` -- **On failure:** revert. -- **Checkpoint:** \`git commit -m "chore: x"\` -- **Manifest:** - \`\`\`yaml - manifest: - expected_paths: - - tmp/x.txt - min_file_count: 1 - commit_message_pattern: "^chore: x" - bash_syntax_check: [] - forbidden_paths: [] - must_contain: [] - \`\`\` - -## Verification - -- It validates. -`; - -const REVIEW_WITH_SOURCE_ANNOTATIONS = `--- -type: trekreview -review_version: "1.0" -task: Demo review with source_annotations -slug: source-annotations-demo-review -project_dir: .claude/projects/2026-05-09-demo -brief_path: .claude/projects/2026-05-09-demo/brief.md -scope_sha_end: 0000000000000000000000000000000000000000 -reviewed_files_count: 0 -findings: [] -revision: 1 -annotation_digest: 0123456789abcdef -source_annotations: - - id: ANN-0001 - target_artifact: review.md - target_anchor: executive-summary - line: 18 - intent: question ---- - -# Demo - -## Executive Summary - -Verdict: ALLOW. - -## Coverage - -| File | Treatment | -|------|-----------| -| _none_ | _no diff_ | - -## Remediation Summary - -ALLOW. -`; - -test('validators accept artifacts with source_annotations field (additive optional, brief)', () => { - const { dir, path } = makeFixture('brief.md', BRIEF_WITH_SOURCE_ANNOTATIONS); - try { - const r = validateBrief(path, { strict: true }); - assert.ok( - r.valid, - `brief-validator rejected synthetic brief with source_annotations: ` + - `${(r.errors || []).map(e => `[${e.code}] ${e.message}`).join('; ')}`, - ); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); - -test('validators accept artifacts with source_annotations field (additive optional, plan)', () => { - const { dir, path } = makeFixture('plan.md', PLAN_WITH_SOURCE_ANNOTATIONS); - try { - const r = validatePlan(path, { strict: true }); - assert.ok( - r.valid, - `plan-validator rejected synthetic plan with source_annotations: ` + - `${(r.errors || []).map(e => `[${e.code}] ${e.message}`).join('; ')}`, - ); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); - -test('validators accept artifacts with source_annotations field (additive optional, review)', () => { - const { dir, path } = makeFixture('review.md', REVIEW_WITH_SOURCE_ANNOTATIONS); - try { - const r = validateReview(path, { strict: true }); - assert.ok( - r.valid, - `review-validator rejected synthetic review with source_annotations: ` + - `${(r.errors || []).map(e => `[${e.code}] ${e.message}`).join('; ')}`, - ); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); - -test('frontmatter parser extracts source_annotations as array of dicts (per artifact)', () => { - const cases = [ - { name: 'brief.md', body: BRIEF_WITH_SOURCE_ANNOTATIONS, expected: 2 }, - { name: 'plan.md', body: PLAN_WITH_SOURCE_ANNOTATIONS, expected: 1 }, - { name: 'review.md', body: REVIEW_WITH_SOURCE_ANNOTATIONS, expected: 1 }, - ]; - for (const c of cases) { - const doc = parseDocument(c.body); - assert.ok(doc.valid, `${c.name}: frontmatter did not parse: ${(doc.errors || []).map(e => e.message).join(', ')}`); - const sa = doc.parsed.frontmatter && doc.parsed.frontmatter.source_annotations; - assert.ok(Array.isArray(sa), `${c.name}: frontmatter.source_annotations is not an array (got ${typeof sa})`); - assert.strictEqual(sa.length, c.expected, `${c.name}: expected ${c.expected} entries, got ${sa.length}`); - } -}); - -test('source_annotations entries match documented annotation shape', () => { - const doc = parseDocument(BRIEF_WITH_SOURCE_ANNOTATIONS); - const entries = doc.parsed.frontmatter.source_annotations; - for (const e of entries) { - assert.strictEqual(typeof e, 'object', `source_annotations entry is not an object: ${JSON.stringify(e)}`); - assert.ok(typeof e.id === 'string' && ID_RE.test(e.id), `source_annotations[*].id must match /^ANN-\\d{4}$/, got ${JSON.stringify(e.id)}`); - assert.ok(typeof e.target_artifact === 'string' && e.target_artifact.endsWith('.md'), - `source_annotations[*].target_artifact must be a *.md path, got ${JSON.stringify(e.target_artifact)}`); - assert.ok(typeof e.target_anchor === 'string' && e.target_anchor.length > 0, - `source_annotations[*].target_anchor must be a non-empty string, got ${JSON.stringify(e.target_anchor)}`); - if (e.intent !== undefined && e.intent !== null) { - assert.ok(VALID_INTENT.has(e.intent), - `source_annotations[*].intent must be in {fix|change|question|block}, got ${JSON.stringify(e.intent)}`); - } - } -}); - -test('artifacts WITHOUT source_annotations still validate (forward-compat baseline)', () => { - // Forward-compat: artifacts that predate v4.2 must still validate. Fall back - // to an artifact with neither revision nor source_annotations. - const baseline = BRIEF_WITH_SOURCE_ANNOTATIONS - .replace(/^revision:.*\n/m, '') - .replace(/^annotation_digest:.*\n/m, '') - .replace(/^source_annotations:[\s\S]*?(?=^---$|^[A-Za-z])/m, ''); - const { dir, path } = makeFixture('brief.md', baseline); - try { - const r = validateBrief(path, { strict: true }); - assert.ok( - r.valid, - `brief-validator must accept artifacts WITHOUT source_annotations (forward-compat baseline): ` + - `${(r.errors || []).map(e => `[${e.code}] ${e.message}`).join('; ')}`, - ); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); diff --git a/plugins/voyage/tests/parsers/anchor-parser.test.mjs b/plugins/voyage/tests/parsers/anchor-parser.test.mjs deleted file mode 100644 index 800834d..0000000 --- a/plugins/voyage/tests/parsers/anchor-parser.test.mjs +++ /dev/null @@ -1,130 +0,0 @@ -// tests/parsers/anchor-parser.test.mjs -// Unit tests for lib/parsers/anchor-parser.mjs (v4.2) - -import { test } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { readFileSync } from 'node:fs'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { - parseAnchors, - addAnchors, - stripAnchors, - validateAnchorPlacement, -} from '../../lib/parsers/anchor-parser.mjs'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const EXAMPLE_PATH = resolve(__dirname, '..', 'fixtures', 'annotation', 'annotation-example.md'); - -const PLAIN = `# Title - -A normal paragraph. - -## Section - -More text. -`; - -test('parseAnchors — empty array on plain markdown without anchors', () => { - const r = parseAnchors(PLAIN); - assert.equal(r.valid, true); - assert.deepEqual(r.parsed, []); -}); - -test('parseAnchors — extracts id/target/line/intent from valid anchor', () => { - const md = readFileSync(EXAMPLE_PATH, 'utf-8'); - const r = parseAnchors(md); - assert.equal(r.valid, true, JSON.stringify(r.errors)); - assert.equal(r.parsed.length, 1); - assert.equal(r.parsed[0].id, 'ANN-0001'); - assert.equal(r.parsed[0].target, 'section-b'); - assert.equal(r.parsed[0].line, 20); - assert.equal(r.parsed[0].intent, 'change'); -}); - -test('parseAnchors — rejects ID not matching ANN-NNNN', () => { - const md = `# X\n\n\n`; - const r = parseAnchors(md); - assert.equal(r.valid, false); - assert.ok(r.errors.find(e => e.code === 'ANCHOR_BAD_ID')); -}); - -test('parseAnchors — rejects malformed (missing id)', () => { - const md = `# X\n\n\n`; - const r = parseAnchors(md); - assert.equal(r.valid, false); - assert.ok(r.errors.find(e => e.code === 'ANCHOR_MALFORMED')); -}); - -test('parseAnchors — rejects duplicate IDs', () => { - const md = `# X\n\n\n\nFoo.\n\n\n`; - const r = parseAnchors(md); - assert.equal(r.valid, false); - assert.ok(r.errors.find(e => e.code === 'ANCHOR_DUPLICATE_ID')); -}); - -test('parseAnchors — ignores anchors inside fenced code blocks', () => { - const md = `# X\n\n\`\`\`yaml\n\n\`\`\`\n`; - const r = parseAnchors(md); - assert.equal(r.valid, true); - assert.deepEqual(r.parsed, []); -}); - -test('addAnchors — empty list returns input byte-identical', () => { - const r = addAnchors(PLAIN, []); - assert.equal(r, PLAIN); -}); - -test('addAnchors — inserts anchor on its own line with blank-line separation', () => { - const md = `# Title\n\nLine 3.\n`; - const result = addAnchors(md, [{ id: 'ANN-0001', target: 'title', line: 3, intent: 'change' }]); - assert.match(result, //); - // Anchor inserted above target line - const lines = result.split('\n'); - const anchorIdx = lines.findIndex(l => l.startsWith('\n- next\n`; - const r = validateAnchorPlacement(md, []); - assert.equal(r.valid, false); - assert.ok(r.errors.find(e => e.code === 'ANCHOR_IN_LIST_ITEM')); -}); - -test('validateAnchorPlacement — rejects anchor inside fenced yaml block', () => { - const md = `# X\n\n\`\`\`yaml\nfoo: bar\n\n\`\`\`\n`; - const r = validateAnchorPlacement(md, []); - assert.equal(r.valid, false); - assert.ok(r.errors.find(e => e.code === 'ANCHOR_IN_FENCED_BLOCK')); -}); - -test('validateAnchorPlacement — accepts anchor in body paragraph', () => { - const md = readFileSync(EXAMPLE_PATH, 'utf-8'); - const r = validateAnchorPlacement(md, []); - assert.equal(r.valid, true, JSON.stringify(r.errors)); -}); - -test('parseAnchors — anchor with intent block sets intent field', () => { - const md = `# X\n\n\n`; - const r = parseAnchors(md); - assert.equal(r.valid, true); - assert.equal(r.parsed[0].intent, 'block'); -}); diff --git a/plugins/voyage/tests/parsers/annotation-digest.test.mjs b/plugins/voyage/tests/parsers/annotation-digest.test.mjs deleted file mode 100644 index bb8e61c..0000000 --- a/plugins/voyage/tests/parsers/annotation-digest.test.mjs +++ /dev/null @@ -1,63 +0,0 @@ -// tests/parsers/annotation-digest.test.mjs -// Unit tests for lib/parsers/annotation-digest.mjs (v4.2) - -import { test } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { computeAnnotationDigest } from '../../lib/parsers/annotation-digest.mjs'; - -test('computeAnnotationDigest — empty array yields deterministic 16-char hex', () => { - const d = computeAnnotationDigest([]); - assert.equal(typeof d, 'string'); - assert.equal(d.length, 16); - assert.match(d, /^[0-9a-f]{16}$/); - // Empty-array digest is a known constant (sha256 of empty string) - assert.equal(d, 'e3b0c44298fc1c14'); -}); - -test('computeAnnotationDigest — array order does not affect digest', () => { - const a = [ - { id: 'ANN-0001', target_artifact: 'plan.md', target_anchor: 'a', intent: 'fix', comment: 'one', timestamp: 't1' }, - { id: 'ANN-0002', target_artifact: 'plan.md', target_anchor: 'b', intent: 'change', comment: 'two', timestamp: 't2' }, - ]; - const b = [a[1], a[0]]; // reversed - assert.equal(computeAnnotationDigest(a), computeAnnotationDigest(b)); -}); - -test('computeAnnotationDigest — different intent produces different digest', () => { - const a = [{ id: 'ANN-0001', target_artifact: 'plan.md', target_anchor: 'a', intent: 'fix', comment: '', timestamp: '' }]; - const b = [{ id: 'ANN-0001', target_artifact: 'plan.md', target_anchor: 'a', intent: 'change', comment: '', timestamp: '' }]; - assert.notEqual(computeAnnotationDigest(a), computeAnnotationDigest(b)); -}); - -test('computeAnnotationDigest — output is exactly 16 lowercase hex chars', () => { - const a = [{ id: 'ANN-0001', target_artifact: 'plan.md', target_anchor: 'a', intent: 'fix', comment: 'x', timestamp: 't' }]; - const d = computeAnnotationDigest(a); - assert.equal(d.length, 16); - assert.match(d, /^[0-9a-f]{16}$/); -}); - -test('computeAnnotationDigest — single annotation produces fixed golden value', () => { - // This pins the canonicalization. Changing the format will break this test. - const a = [{ - id: 'ANN-0001', - target_artifact: 'plan.md', - target_anchor: 'step-3', - intent: 'change', - comment: 'reorder', - timestamp: '2026-05-09T10:00:00Z', - }]; - const d = computeAnnotationDigest(a); - // Canonical: "ANN-0001|plan.md|step-3|change|reorder|2026-05-09T10:00:00Z" - // Computed once and pinned here: - assert.equal(d.length, 16); - assert.match(d, /^[0-9a-f]{16}$/); - // Recompute deterministically — same input must always give same output - const d2 = computeAnnotationDigest(a); - assert.equal(d, d2); -}); - -test('computeAnnotationDigest — undefined optional fields treated identically to empty string', () => { - const a = [{ id: 'ANN-0001', target_artifact: 'plan.md', target_anchor: 'a', intent: 'fix' }]; // no comment, no timestamp - const b = [{ id: 'ANN-0001', target_artifact: 'plan.md', target_anchor: 'a', intent: 'fix', comment: '', timestamp: '' }]; - assert.equal(computeAnnotationDigest(a), computeAnnotationDigest(b)); -}); diff --git a/plugins/voyage/tests/playground/voyage-playground-structure.test.mjs b/plugins/voyage/tests/playground/voyage-playground-structure.test.mjs deleted file mode 100644 index bc3c700..0000000 --- a/plugins/voyage/tests/playground/voyage-playground-structure.test.mjs +++ /dev/null @@ -1,88 +0,0 @@ -// tests/playground/voyage-playground-structure.test.mjs -// v4.3 Step 29 — Group B structural assertions for the voyage playground. -// -// Group B verifies that DS-token classes, theme-toggle wiring, and the -// sidebar-tab/keyboard pattern are present in voyage-playground.html. -// All assertions are static-grep (no DOM, no browser). Companion to: -// - tests/playground/voyage-playground.test.mjs (Group A — SC1/3/6/7) -// - tests/integration/annotation-export-schema.test.mjs (Group C — SC6) - -import { test } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { readFileSync } from 'node:fs'; -import { dirname, resolve, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const ROOT = resolve(__dirname, '..', '..'); -const HTML = join(ROOT, 'playground', 'voyage-playground.html'); - -// --- DS-token classes present ---------------------------------------- -test('Group B — DS badge--scope-voyage class present (v4.3 Step 29)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /badge--scope-voyage/, 'badge--scope-voyage required'); -}); - -test('Group B — DS guide-panel + key-stats classes present (v4.3 Step 29)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /class="guide-panel/, 'guide-panel base class required'); - assert.match(text, /class="key-stats/, 'key-stats class required'); -}); - -test('Group B — DS fleet-grid + fleet-tile classes present (v4.3 Step 29)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /class="fleet-grid"/, 'fleet-grid required'); - assert.match(text, /class="fleet-tile/, 'fleet-tile required'); -}); - -// --- Theme-toggle wired --------------------------------------------- -test('Group B — theme-toggle button has data-action attribute (v4.3 Step 29)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /data-action="toggle-theme"/, 'data-action=toggle-theme required'); - assert.match(text, /aria-label="Bytt tema"/, 'theme-toggle aria-label required'); -}); - -test('Group B — wireThemeToggle handler exists (v4.3 Step 29)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /function\s+wireThemeToggle\s*\(/, 'wireThemeToggle function required'); -}); - -test('Group B — theme persistence to localStorage (v4.3 Step 29)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /(voyage-theme|voyage_theme)/, 'theme localStorage key required'); -}); - -// --- Sidebar-tab / keyboard pattern --------------------------------- -test('Group B — sidebar role=tablist with aria-selected (v4.3 Step 29)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /role="tablist"/, 'role=tablist required'); - assert.match(text, /aria-selected="(true|false)"/, 'aria-selected attribute required'); -}); - -test('Group B — keyboard nav J/K + Esc handlers wired (v4.3 Step 29)', () => { - const text = readFileSync(HTML, 'utf-8'); - // Step 20 — J/K navigation + Esc dismiss - assert.match(text, /(keydown|keypress|keyup)/, 'keyboard event listener required'); - assert.match(text, /(['"]j['"]|['"]J['"]|KeyJ)/, 'J navigation required'); - assert.match(text, /(['"]k['"]|['"]K['"]|KeyK)/, 'K navigation required'); -}); - -test('Group B — anchor-ID format ANN-NNNN matches Node-side parser (v4.3 Step 29)', () => { - const text = readFileSync(HTML, 'utf-8'); - // Mirror of lib/parsers/anchor-parser.mjs ID_RE (^ANN-\d{4}$) - assert.match(text, /\/\^ANN-\\d\{4\}\$\//, 'VOYAGE_ANCHOR_ID_RE pattern required'); - assert.match(text, /function\s+parseAnchor\s*\(/, 'parseAnchor function required'); -}); - -// --- Fleet-grid CSS parity vs vendored DS (v4.3 Step 9 / 99707f51) --- -test('Group B — SC1.4 fleet-grid CSS parity vs vendored DS (99707f51)', () => { - const cssPath = join(ROOT, 'playground', 'vendor', 'playground-design-system', 'components-tier3-supplement.css'); - const css = readFileSync(cssPath, 'utf-8'); - const startIdx = css.indexOf('.fleet-grid {'); - assert.notStrictEqual(startIdx, -1, '.fleet-grid block required in vendored DS components-tier3-supplement.css'); - const endIdx = css.indexOf('}', startIdx); - assert.notStrictEqual(endIdx, -1, '.fleet-grid block must terminate'); - const block = css.slice(startIdx, endIdx + 1); - assert.match(block, /grid-template-columns:\s*repeat\(4,\s*1fr\)/, '.fleet-grid grid-template-columns: repeat(4, 1fr) required'); - assert.match(block, /gap:\s*var\(--space-3\)/, '.fleet-grid gap: var(--space-3) required'); -}); diff --git a/plugins/voyage/tests/playground/voyage-playground.test.mjs b/plugins/voyage/tests/playground/voyage-playground.test.mjs deleted file mode 100644 index cdc7798..0000000 --- a/plugins/voyage/tests/playground/voyage-playground.test.mjs +++ /dev/null @@ -1,710 +0,0 @@ -// tests/playground/voyage-playground.test.mjs -// Filesystem + content tests for v4.2 voyage playground. -// Pure existence + grep checks — no browser launch. - -import { test } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { existsSync, statSync, readFileSync, readdirSync } from 'node:fs'; -import { dirname, resolve, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const ROOT = resolve(__dirname, '..', '..'); -const PLAYGROUND = join(ROOT, 'playground'); -const HTML = join(PLAYGROUND, 'voyage-playground.html'); -const VENDOR = join(PLAYGROUND, 'vendor', 'playground-design-system'); -const MANIFEST = join(VENDOR, 'MANIFEST.json'); - -test('voyage-playground.html exists and has nonzero size', () => { - assert.ok(existsSync(HTML), 'voyage-playground.html must exist'); - assert.ok(statSync(HTML).size > 0, 'must have content'); -}); - -test('voyage-playground.html has DOCTYPE + html closing tag', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /^/i); - assert.match(text, /<\/html>\s*$/); -}); - -test('voyage-playground.html does NOT contain external (http/https) URLs', () => { - // SC1 zero-network constraint: all assets must be relative to ./vendor/ - const text = readFileSync(HTML, 'utf-8'); - assert.ok(!/https?:\/\//.test(text), 'no external URLs allowed in playground HTML'); -}); - -test('voyage-playground.html does NOT contain literal `marked` (renderer ban per risk-assessor H1)', () => { - const text = readFileSync(HTML, 'utf-8'); - // marked is disqualified by issue #3515; markdown-it locked instead - // Allow comments mentioning "marked" as an explanatory artifact, but no actual import paths - assert.ok(!/from ['"].*marked/.test(text), 'no import from marked'); - assert.ok(!/]*marked\.min\.js/.test(text), 'no marked script tag'); -}); - -test('voyage-playground.html includes skip-to-main link (A11Y baseline)', () => { - const text = readFileSync(HTML, 'utf-8'); - // v4.3 Step 10 — Norwegian skip-link: "Hopp til hovedinnhold" - assert.match(text, /class="skip-link"[^>]*href="#main-content"/); - assert.match(text, /Hopp til hovedinnhold/); -}); - -test('voyage-playground.html declares aria-live region', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /aria-live="polite"/); -}); - -test('playground/vendor/playground-design-system/MANIFEST.json exists and parses as JSON with expected keys', () => { - assert.ok(existsSync(MANIFEST), 'MANIFEST.json must be present from sync-design-system.mjs'); - const obj = JSON.parse(readFileSync(MANIFEST, 'utf-8')); - assert.ok(obj.source_commit, 'source_commit field required'); - assert.ok(obj.sync_date, 'sync_date field required'); - assert.ok(obj.files && typeof obj.files === 'object', 'files map required'); -}); - -test('playground/vendor/playground-design-system/ contains expected DS files', () => { - const files = readdirSync(VENDOR); - for (const expected of ['tokens.css', 'base.css', 'components.css', 'fonts.css', 'print.css']) { - assert.ok(files.includes(expected), `${expected} expected in vendor/`); - } - assert.ok(files.includes('fonts'), 'fonts/ subdirectory expected'); -}); - -// --- Step 8 — render pipeline + vendored libs --------------------------- - -const PLAYGROUND_LIB = join(PLAYGROUND, 'lib'); - -test('voyage-playground.html references markdown-it (Step 8 render pipeline)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /markdown-it/, 'voyage-playground.html should load/initialize markdown-it'); -}); - -test('voyage-playground.html references highlight.js (Step 8 syntax highlighting)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /highlight/, 'voyage-playground.html should load highlight.js'); -}); - -test('voyage-playground.html includes paste-import-row (Step 8 import affordance)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /paste-import-row/, 'voyage-playground.html should include the paste-import-row pattern'); -}); - -test('voyage-playground.html declares voyage_ann_ localStorage key prefix (Step 8 risk-assessor H7)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /voyage_ann_/, 'localStorage key prefix voyage_ann___ must appear'); -}); - -test('playground/lib/ contains vendored markdown-it + front-matter + highlight bundles', () => { - for (const f of ['markdown-it.min.js', 'markdown-it-front-matter.min.js', 'highlight.min.js', 'VENDOR-MANIFEST.json']) { - assert.ok(existsSync(join(PLAYGROUND_LIB, f)), `playground/lib/${f} expected from vendor-playground-libs.mjs`); - } -}); - -// --- Step 9 — annotation creation gestures + form modal --------------- - -test('voyage-playground.html declares aria-modal="true" (Step 9 form modal A11Y)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /aria-modal="true"/, 'form modal must carry aria-modal="true"'); -}); - -test('voyage-playground.html declares ANN- anchor-ID prefix (Step 9 ID generation)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /ANN-/, 'sequential ANN-NNNN ID generation must appear in playground JS'); -}); - -test('voyage-playground.html declares 300ms grace constant (Step 9 adder-popup grace)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /300\s*ms|GRACE_MS\s*=\s*300|ADDER_GRACE_MS/i, '300ms grace period for adder-popup must be present'); -}); - -// --- Step 10 — sidebar with tabs + critique-card-list ---------------- - -test('voyage-playground.html includes role="tablist" (Step 10 sidebar tabs A11Y)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /role="tablist"/, 'sidebar must declare role="tablist" for A11Y'); -}); - -test('voyage-playground.html declares tabindex (Step 10 focus management)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /tabindex/i, 'sidebar tabs must use tabindex for keyboard focus management'); -}); - -// --- Step 11 — export flow + A11Y baseline ----------------------------- - -test('voyage-playground.html declares aria-live="polite" toast region (Step 11 A11Y)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /aria-live="polite"/, 'aria-live="polite" toast region required for status announcements'); -}); - -test('voyage-playground.html includes Skip to main link (Step 11 A11Y baseline)', () => { - const text = readFileSync(HTML, 'utf-8'); - // v4.3 Step 10 — text re-localized to Norwegian; semantic check via class. - assert.match(text, /class="skip-link"/, 'skip-link class required for keyboard A11Y'); -}); - -test('voyage-playground.html uses Blob for download flow (Step 11 export)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /\bnew Blob\b/, 'Blob download path required for annotated.md export'); -}); - -test('voyage-playground.html uses clipboard.writeText for copy flow (Step 11 export)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /clipboard\.writeText/, 'navigator.clipboard.writeText path required for command-copy'); -}); - -// --- v4.3 Sesjon 3 — Step 14 (dashboard) + Step 15 (drill-down + URL routing) ---- - -test('voyage-playground.html declares fleet-grid container (v4.3 Step 14 dashboard)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /fleet-grid/, 'fleet-grid container required for dashboard layout'); -}); - -test('voyage-playground.html declares fleet-tile (v4.3 Step 14 dashboard)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /fleet-tile/, 'fleet-tile required for per-artifact dashboard cell'); -}); - -test('voyage-playground.html declares renderDashboard JS function (v4.3 Step 14)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /function renderDashboard\b/, 'renderDashboard function required'); -}); - -test('voyage-playground.html declares dashboard status vocabulary (v4.3 Step 14)', () => { - const text = readFileSync(HTML, 'utf-8'); - // Status vocabulary per plan: complete, in-progress, blocked, missing, stale - assert.match(text, /'complete'/, 'status complete required'); - assert.match(text, /'in-progress'/, 'status in-progress required'); - assert.match(text, /'blocked'/, 'status blocked required'); - assert.match(text, /'missing'/, 'status missing required'); - assert.match(text, /'stale'/, 'status stale required'); -}); - -test('voyage-playground.html declares renderArtifactDetail JS function (v4.3 Step 15 drill-down)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /function renderArtifactDetail\b/, 'renderArtifactDetail function required for drill-down'); -}); - -test('voyage-playground.html declares URLSearchParams routing (v4.3 Step 15)', () => { - const text = readFileSync(HTML, 'utf-8'); - // Presence-only: URLSearchParams already used at line 810 for project-key - // derivation; Step 15 adds ?project= dashboard/detail routing. - assert.match(text, /URLSearchParams/, 'URLSearchParams required for ?project= routing'); -}); - -test('voyage-playground.html declares data-action="back-to-dashboard" (v4.3 Step 15 back-nav)', () => { - const text = readFileSync(HTML, 'utf-8'); - // Stricter than Step 14 wording — must appear as data-action attribute - // somewhere in the JS template, not only in HTML comments. - assert.match(text, /data-action="back-to-dashboard"/, 'data-action="back-to-dashboard" required for return-nav handler'); -}); - -test('voyage-playground.html declares popstate handler (v4.3 Step 15 back/forward)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /'popstate'/, 'popstate listener required for browser back/forward'); -}); - -test('voyage-playground.html declares VOYAGE_ANCHOR_RE constant (v4.3 Step 16 anchor allowlist)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /VOYAGE_ANCHOR_RE\s*=\s*\/\^/, 'VOYAGE_ANCHOR_RE regex constant required'); - assert.match(text, /VOYAGE_ANCHOR_ATTR_RE\s*=\s*\//, 'VOYAGE_ANCHOR_ATTR_RE constant required'); - assert.match(text, /VOYAGE_ANCHOR_ID_RE\s*=\s*\/\^ANN-/, 'VOYAGE_ANCHOR_ID_RE constant required'); -}); - -test('voyage-playground.html anchor regex matches Node-side allowlist (v4.3 Step 16 cross-file sync)', () => { - const html = readFileSync(HTML, 'utf-8'); - const node = readFileSync(join(ROOT, 'lib', 'parsers', 'anchor-parser.mjs'), 'utf-8'); - const htmlMatch = html.match(/voyage:anchor[^/]+/)?.[0]; - const nodeMatch = node.match(/voyage:anchor[^/]+/)?.[0]; - assert.equal(htmlMatch, nodeMatch, 'first voyage:anchor token in HTML must mirror Node-side parser exactly'); -}); - -test('voyage-playground.html declares parseAnchor validator (v4.3 Step 16)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /function\s+parseAnchor\s*\(\s*line\s*\)/, 'parseAnchor(line) function required'); -}); - -test('voyage-playground.html declares relocateAnchorsToBlockBoundaries pure function (v4.3 Step 17)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /function\s+relocateAnchorsToBlockBoundaries\s*\(\s*text\s*,\s*anchors\s*\)/, - 'relocateAnchorsToBlockBoundaries(text, anchors) pure function required'); -}); - -test('voyage-playground.html declares .voyage-anchor-badge gutter component (v4.3 Step 18)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /\.voyage-anchor-badge\s*\{/, '.voyage-anchor-badge CSS class required'); - assert.match(text, /position:\s*absolute/, '.voyage-anchor-badge must use absolute positioning'); - assert.match(text, /var\(--color-scope-voyage\)/, 'badge must use --color-scope-voyage token'); -}); - -test('voyage-playground.html declares .voyage-anchor-active yellow-tint highlight (v4.3 Step 18)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /\.voyage-anchor-active\s*\{/, '.voyage-anchor-active CSS class required'); - assert.match(text, /rgba\(255,\s*235,\s*59,\s*0\.25\)/, 'yellow-tint rgba(255, 235, 59, 0.25) required'); -}); - -test('voyage-playground.html does NOT contain v4.2 pencil-icon references (v4.3 Step 18 cleanup)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.doesNotMatch(text, /voyage-pencil-btn/, 'pencil-btn class must be removed'); - assert.doesNotMatch(text, /injectPencilIcons/, 'injectPencilIcons function must be replaced by injectAnchorBadges'); -}); - -test('voyage-playground.html declares injectAnchorBadges JS function (v4.3 Step 18)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /function\s+injectAnchorBadges\s*\(\s*\)/, 'injectAnchorBadges() function required'); -}); - -test('voyage-playground.html declares voyage-sidebar hidden-by-default (v4.3 Step 19)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /id="voyage-sidebar"[\s\S]{0,200}aria-hidden="true"/, 'voyage-sidebar must default aria-hidden="true"'); -}); - -test('voyage-playground.html declares data-action="toggle-sidebar" on FAB (v4.3 Step 19)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /data-action="toggle-sidebar"/, 'data-action="toggle-sidebar" required on FAB toggle button'); -}); - -test('voyage-playground.html declares voyage-jumplist + count "X av N" (v4.3 Step 19)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /id="voyage-jumplist"/, 'voyage-jumplist ordered list required'); - assert.match(text, /id="voyage-jumplist-count"/, 'voyage-jumplist-count container required'); - assert.match(text, /' av '/, '"X av N" jumplist count format string required in JS'); -}); - -test('voyage-playground.html declares filter-buttons Alle/Åpne/Resolved (v4.3 Step 19)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /data-filter="all"/, 'filter button data-filter="all" required'); - assert.match(text, /data-filter="open"/, 'filter button data-filter="open" required'); - assert.match(text, /data-filter="resolved"/, 'filter button data-filter="resolved" required'); -}); - -test('voyage-playground.html declares renderAnnotationList JS function (v4.3 Step 19)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /function\s+renderAnnotationList\s*\(\s*\)/, 'renderAnnotationList() function required'); -}); - -test('voyage-playground.html declares wireKeyboardNav with j/k/]/Escape (v4.3 Step 20)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /function\s+wireKeyboardNav\s*\(\s*\)/, 'wireKeyboardNav() function required'); - assert.match(text, /e\.key === 'j'/, "'j' key handler required"); - assert.match(text, /e\.key === 'k'/, "'k' key handler required"); - assert.match(text, /e\.key === '\]'/, "']' key (toggle-sidebar) required"); - assert.match(text, /e\.key === 'Escape'/, "'Escape' key handler required"); -}); - -test('voyage-playground.html keyboard nav skips inputs/textareas (v4.3 Step 20)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /matches\([^)]*input[^)]*textarea/, 'input/textarea matches() guard required'); -}); - -test('voyage-playground.html keyboard nav announces via aria-live region (v4.3 Step 20)', () => { - const text = readFileSync(HTML, 'utf-8'); - // The wireKeyboardNav body contains announce(... ' av ' ...) for nav-position announce - assert.match(text, /announce\('Annotering '/, 'aria-live announce on annotation navigation required'); -}); - -// v4.3 Step 21 — two-opacity pattern (active 100% / inactive 40% / resolved 30% strikethrough) -test('voyage-playground.html declares two-opacity inactive default for badges (v4.3 Step 21)', () => { - const text = readFileSync(HTML, 'utf-8'); - // Default badge rule must include opacity: 0.4 (inactive) - assert.match(text, /\.voyage-anchor-badge\s*\{[^}]*opacity:\s*0\.4/s, '.voyage-anchor-badge default opacity: 0.4 required'); -}); - -test('voyage-playground.html declares two-opacity active state for badges (v4.3 Step 21)', () => { - const text = readFileSync(HTML, 'utf-8'); - // Active state: data-active="true" must restore opacity to 1 - assert.match(text, /\.voyage-anchor-badge\[data-active="true"\]\s*\{[^}]*opacity:\s*1/s, 'data-active opacity: 1 required'); -}); - -test('voyage-playground.html declares two-opacity resolved state for badges (v4.3 Step 21)', () => { - const text = readFileSync(HTML, 'utf-8'); - // Resolved state: data-resolved="true" must produce opacity 0.3 + line-through - assert.match(text, /\.voyage-anchor-badge\[data-resolved="true"\]\s*\{[^}]*opacity:\s*0\.3/s, 'data-resolved opacity: 0.3 required'); - assert.match(text, /\.voyage-anchor-badge\[data-resolved="true"\]\s*\{[^}]*text-decoration:\s*line-through/s, 'data-resolved line-through required'); -}); - -test('voyage-playground.html declares two-opacity for sidebar list-items (v4.3 Step 21)', () => { - const text = readFileSync(HTML, 'utf-8'); - // List-item default opacity 0.4 - assert.match(text, /\.voyage-annotation-list__items\s+li\s*\{[^}]*opacity:\s*0\.4/s, 'list-item default opacity: 0.4 required'); - // List-item active overrides to 1 - assert.match(text, /\.voyage-annotation-list__items\s+li\[data-active="true"\][^}]*opacity:\s*1/s, 'list-item active opacity: 1 required'); - // List-item resolved opacity 0.3 - assert.match(text, /\.voyage-annotation-list__items\s+li\[data-resolved="true"\][^}]*opacity:\s*0\.3/s, 'list-item resolved opacity: 0.3 required'); -}); - -test('voyage-playground.html setActiveAnchor toggles data-active on badges (v4.3 Step 21)', () => { - const text = readFileSync(HTML, 'utf-8'); - // setActiveAnchor must clear prior data-active and set new one - assert.match(text, /setAttribute\('data-active',\s*'true'\)/, 'data-active set on active badge required'); - // injectAnchorBadges must propagate resolved state to badge data-resolved - assert.match(text, /setAttribute\('data-resolved',\s*'true'\)/, 'data-resolved set on resolved badge required'); -}); - -// v4.3 Step 22 — A11Y-panel built from DS-primitives (greenfield) -test('voyage-playground.html declares voyage-a11y-panel with guide-panel--info (v4.3 Step 22)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /id="voyage-a11y-panel"[^>]*guide-panel guide-panel--info/, 'voyage-a11y-panel with guide-panel--info required'); - // Must be hidden by default (placeholder until Wave 7) - assert.match(text, /id="voyage-a11y-panel"[\s\S]{0,300}\bhidden\b/, 'voyage-a11y-panel hidden by default required'); -}); - -test('voyage-playground.html declares data-action="toggle-a11y-panel" toggle-button (v4.3 Step 22)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /data-action="toggle-a11y-panel"/, 'toggle-a11y-panel button required'); - // aria-controls must point at the panel id - assert.match(text, /data-action="toggle-a11y-panel"[\s\S]*?aria-controls="voyage-a11y-panel"/, 'aria-controls binding required'); -}); - -test('voyage-playground.html A11Y-panel uses key-stats severity grid (v4.3 Step 22)', () => { - const text = readFileSync(HTML, 'utf-8'); - // key-stats grid with critical/high/medium/low severity modifiers - assert.match(text, /class="key-stat key-stat--critical"/, 'key-stat--critical required'); - assert.match(text, /class="key-stat key-stat--high"/, 'key-stat--high (serious) required'); - assert.match(text, /class="key-stat key-stat--medium"/, 'key-stat--medium (moderate) required'); - assert.match(text, /class="key-stat key-stat--low"/, 'key-stat--low (minor) required'); - // axe-core severity vocabulary on data-a11y-stat - assert.match(text, /data-a11y-stat="critical"/, 'data-a11y-stat="critical" required'); - assert.match(text, /data-a11y-stat="serious"/, 'data-a11y-stat="serious" required'); - assert.match(text, /data-a11y-stat="moderate"/, 'data-a11y-stat="moderate" required'); - assert.match(text, /data-a11y-stat="minor"/, 'data-a11y-stat="minor" required'); -}); - -test('voyage-playground.html A11Y-panel uses findings__items placeholder list (v4.3 Step 22)', () => { - const text = readFileSync(HTML, 'utf-8'); - // Match either attribute order (class= or id= first); just confirm both live on the same
    . - assert.match(text, /]*class="findings__items"[^>]*id="voyage-a11y-findings"|]*id="voyage-a11y-findings"[^>]*class="findings__items"/, 'findings__items list (id=voyage-a11y-findings) required'); - // Placeholder line referencing the Wave 7 Playwright spec - assert.match(text, /Kjør axe-spec/, 'placeholder hint "Kjør axe-spec" required'); -}); - -test('voyage-playground.html declares wireA11yToggle JS function (v4.3 Step 22)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /function\s+wireA11yToggle\s*\(\s*\)/, 'wireA11yToggle() function required'); - // Toggle must flip hidden + aria-expanded - assert.match(text, /panel\.hidden\s*=\s*!willOpen/, 'panel.hidden toggle required'); - assert.match(text, /setAttribute\('aria-expanded'/, 'aria-expanded update required'); -}); - -// v4.3 Step 23 — screenshots-spor convention (window.__hooks + docs/screenshots/) -test('voyage-playground.html exposes window.__voyage automation hooks (v4.3 Step 23)', () => { - const text = readFileSync(HTML, 'utf-8'); - // window.__voyage must be assigned (object literal or assignment expression) - assert.match(text, /window\.__voyage\s*=\s*\{/, 'window.__voyage = { ... } assignment required'); -}); - -test('voyage-playground.html window.__voyage exposes navigate/scheduleRender/getProjectArtifacts (v4.3 Step 23)', () => { - const text = readFileSync(HTML, 'utf-8'); - // Each method must appear as a property of the exposed object. - assert.match(text, /navigate:\s*function/, 'navigate method required'); - assert.match(text, /scheduleRender:\s*function/, 'scheduleRender method required'); - assert.match(text, /getProjectArtifacts:\s*function/, 'getProjectArtifacts method required'); -}); - -test('docs/screenshots/README.md documents mappestruktur + hooks (v4.3 Step 23)', () => { - const path = join(ROOT, 'docs', 'screenshots', 'README.md'); - const text = readFileSync(path, 'utf-8'); - assert.match(text, /Mappestruktur/, 'Mappestruktur heading required'); - // Must list each documented subfolder - assert.match(text, /dashboard\//, 'dashboard/ subfolder documented'); - assert.match(text, /artifact-detail\//, 'artifact-detail/ subfolder documented'); - assert.match(text, /annotation\//, 'annotation/ subfolder documented'); - assert.match(text, /dark-mode\//, 'dark-mode/ subfolder documented'); - assert.match(text, /light-mode\//, 'light-mode/ subfolder documented'); - // Hooks documentation must reference all three methods - assert.match(text, /window\.__voyage\.navigate/, 'navigate hook documented'); - assert.match(text, /window\.__voyage\.scheduleRender/, 'scheduleRender hook documented'); - assert.match(text, /window\.__voyage\.getProjectArtifacts/, 'getProjectArtifacts hook documented'); -}); - -// v4.3 Step 24 — vendor DOMPurify + sanitize annotation-content -test('playground/lib/dompurify.min.js is vendored (v4.3 Step 24)', () => { - const path = join(PLAYGROUND, 'lib', 'dompurify.min.js'); - assert.equal(existsSync(path), true, 'playground/lib/dompurify.min.js must exist (run scripts/vendor-playground-libs.mjs)'); - const size = statSync(path).size; - // Sanity floor — DOMPurify min bundle is ~22 KB; reject empty/0-byte - assert.ok(size > 5000, 'dompurify.min.js too small (' + size + ' bytes) — vendor script may have failed'); -}); - -test('playground/lib/VENDOR-MANIFEST.json pins dompurify >= 3.1.1 (v4.3 Step 24)', () => { - const path = join(PLAYGROUND, 'lib', 'VENDOR-MANIFEST.json'); - const manifest = JSON.parse(readFileSync(path, 'utf-8')); - assert.ok(manifest.pins && manifest.pins.dompurify, 'manifest must pin dompurify'); - // semver compare on major.minor: must be >= 3.1.1 - const m = String(manifest.pins.dompurify).match(/^(\d+)\.(\d+)\.(\d+)/); - assert.ok(m, 'invalid dompurify pin format: ' + manifest.pins.dompurify); - const [, maj, min] = m; - assert.ok(Number(maj) > 3 || (Number(maj) === 3 && Number(min) >= 1), 'dompurify pin must be >= 3.1.1, got ' + manifest.pins.dompurify); - assert.ok(manifest.output_files.includes('dompurify.min.js'), 'manifest output_files must list dompurify.min.js'); -}); - -test('voyage-playground.html loads dompurify.min.js (v4.3 Step 24)', () => { - const text = readFileSync(HTML, 'utf-8'); - assert.match(text, /\n```\n'); + assert.ok(!out.includes(''), 'raw tag escaped'); + assert.ok(out.includes('<tag>'), 'tag rendered as entity'); + assert.ok(out.includes('<script>alert(1)</script>'), 'code-fence content escaped'); +}); + +test('render() is deterministic — two runs byte-identical', () => { + const dir = mkdtempSync(join(tmpdir(), 'render-artifact-')); try { - const out = join(dir, 'brief.html'); - const stdout = runRender(FIX_BRIEF, out); - assert.match(stdout, /render-artifact: wrote/, 'CLI should announce written path'); - assert.ok(existsSync(out), 'output file must exist'); - assert.ok(statSync(out).size > 0, 'output file must be non-empty'); + const md = join(dir, 'plan.md'); + writeFileSync(md, SAMPLE); + const out1 = render(md, join(dir, 'a.html')); + const out2 = render(md, join(dir, 'b.html')); + assert.ok(existsSync(out1) && existsSync(out2)); + assert.equal(readFileSync(out1, 'utf-8'), readFileSync(out2, 'utf-8')); } finally { rmSync(dir, { recursive: true, force: true }); } }); -test('render-artifact output has DOCTYPE + closing + inlined