diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml
index b83eed96..b79f92b8 100644
--- a/.github/workflows/actionlint.yml
+++ b/.github/workflows/actionlint.yml
@@ -50,3 +50,10 @@ jobs:
bash download-actionlint.bash
./actionlint -color
shell: bash
+
+ - name: Check actions are SHA-pinned
+ # Fail if any workflow step uses a mutable action ref (a branch like
+ # @main or a tag like @v4) instead of a full commit SHA, so a repointed
+ # upstream tag cannot silently run new code in CI.
+ run: bash scripts/check-action-pinning.sh
+ shell: bash
diff --git a/.github/workflows/catalog-drift.yml b/.github/workflows/catalog-drift.yml
new file mode 100644
index 00000000..77bd85a0
--- /dev/null
+++ b/.github/workflows/catalog-drift.yml
@@ -0,0 +1,42 @@
+name: Catalog drift
+
+# Fails when the module catalog in README.md or the npm-version region in
+# DELIVERY_HUB.html has drifted from the real package.json + npm state.
+# Run `node scripts/ariada-bus-catalog.mjs --fix` locally to reconcile.
+
+on:
+ pull_request:
+ branches: [main]
+ paths:
+ - 'packages/**/package.json'
+ - 'README.md'
+ - 'strategy/dashboards/DELIVERY_HUB.html'
+ - 'scripts/ariada-bus-catalog.mjs'
+ - '.github/workflows/catalog-drift.yml'
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ check:
+ name: Module-catalog reconciled
+ runs-on: ubuntu-22.04
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
+
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: '22'
+
+ # Read-only reconciler check — computes drift, writes nothing. Safe on
+ # untrusted PR builds (the fix mode, which rewrites source, never runs in
+ # CI). npm registry is queried live; transient failures degrade a single
+ # package to "source-only" rather than crashing the gate.
+ - name: Check module-catalog drift
+ run: node scripts/ariada-bus-catalog.mjs --check
diff --git a/.github/workflows/content-gate.yml b/.github/workflows/content-gate.yml
index 87e0bb48..ce0bdb5c 100644
--- a/.github/workflows/content-gate.yml
+++ b/.github/workflows/content-gate.yml
@@ -34,7 +34,7 @@ jobs:
# gate's signatures + oracle (the detector must not flag itself).
files=$(git diff --name-only --diff-filter=ACM "$base"...HEAD \
| grep -E '\.(md|mdx|ts|tsx|astro|json|yml|yaml|html|css|sh)$' \
- | grep -vE '/dist/|/node_modules/|pnpm-lock\.yaml|packages/ariada-content-policy/(src/rule-packs|test)/|scan-evidence/|test-report/' \
+ | grep -vE '/dist/|/node_modules/|pnpm-lock\.yaml|packages/ariada-content-policy/(src/rule-packs|test)/|scan-evidence/|test-report/|integrations/mkdocs-ariada/examples/|integrations/rapidapi-ariada/examples/curl|integrations/zeplin-ariada/src/cli|packages/ariada-content-policy/README' \
| tr '\n' ' ')
echo "files=$files" >> "$GITHUB_OUTPUT"
echo "Scanning: ${files:-}"
diff --git a/.github/workflows/eaa-diff.yml b/.github/workflows/eaa-diff.yml
index b58bfe86..78f5a4aa 100644
--- a/.github/workflows/eaa-diff.yml
+++ b/.github/workflows/eaa-diff.yml
@@ -281,7 +281,7 @@ jobs:
# -----------------------------------------------------------------------
- name: Run differential gate
id: diff
- uses: ariada-org/ariada/packages/ariada-diff-action@main
+ uses: ariada-org/ariada/packages/ariada-diff-action@fbf190769af2eb0a5796040e947ba1b134ad8d1e # v0.1.0
with:
head-scan: ${{ steps.resolve.outputs.head-scan }}
base-scan: ${{ steps.resolve.outputs.base-scan }}
diff --git a/.github/workflows/eaa-vercel-diff.yml b/.github/workflows/eaa-vercel-diff.yml
index 00ebf448..95849798 100644
--- a/.github/workflows/eaa-vercel-diff.yml
+++ b/.github/workflows/eaa-vercel-diff.yml
@@ -254,7 +254,7 @@ jobs:
# -----------------------------------------------------------------------
- name: Run differential gate
id: diff
- uses: ariada-org/ariada/packages/ariada-diff-action@main
+ uses: ariada-org/ariada/packages/ariada-diff-action@fbf190769af2eb0a5796040e947ba1b134ad8d1e # v0.1.0
with:
head-scan: ${{ steps.resolve.outputs.head-scan }}
base-scan: ${{ steps.resolve.outputs.base-scan }}
diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml
index 6142145b..8fb9c764 100644
--- a/.github/workflows/gitleaks.yml
+++ b/.github/workflows/gitleaks.yml
@@ -34,7 +34,9 @@ jobs:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
- fetch-depth: 0 # full history for the scheduled scan
+ # Shallow clone for push/PR (only current code matters).
+ # Full history for the weekly schedule scan.
+ fetch-depth: ${{ github.event_name == 'schedule' && 0 || 1 }}
persist-credentials: false
- name: Download gitleaks
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index 8d70b3ec..86604b9e 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -7,6 +7,10 @@ on:
push:
branches: [main]
+concurrency:
+ group: scorecard-${{ github.ref }}
+ cancel-in-progress: true
+
permissions:
contents: read
@@ -15,14 +19,13 @@ jobs:
name: Scorecard analysis
runs-on: ubuntu-22.04
permissions:
- security-events: write
- id-token: write
- contents: read
- actions: read
- # Scorecard uses these read scopes to inspect review/check metadata.
- issues: read
- pull-requests: read
- checks: read
+ security-events: write # Upload Scorecard SARIF to code scanning.
+ id-token: write # Publish signed Scorecard results.
+ contents: read # Read repository contents.
+ actions: read # Inspect workflow metadata for Scorecard checks.
+ issues: read # Inspect issue activity for maintenance signals.
+ pull-requests: read # Inspect PR review metadata.
+ checks: read # Inspect check-run metadata.
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
diff --git a/.gitignore b/.gitignore
index 98be6343..9bb46f9d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,6 +65,11 @@ research/output/phase4bd_cache/
# agent worktrees — local-only scratch dirs for background agents
.claude/worktrees/
+.worktrees/
+
+# legacy research scratch (saved web pages, old docx/pdf reports) — removed
+# from the index; kept on disk locally, never tracked or public-bound
+archive/
# Claude Code orchestrator local state (cron jobs, runtime locks)
.claude/scheduled_tasks.json
@@ -118,3 +123,8 @@ patentomania/nats-data/
var/build-evidence/
__pycache__/
var/
+
+# Codex per-machine config (contains API keys — never commit)
+.codex/config.toml
+.codex/auth.json
+.ariada/
diff --git a/.gitleaks.toml b/.gitleaks.toml
index 159479fa..ede603c5 100644
--- a/.gitleaks.toml
+++ b/.gitleaks.toml
@@ -15,8 +15,17 @@ title = "ariada gitleaks config"
useDefault = true
[allowlist]
-description = "Allow synthetic secret-shaped fixtures used by detector tests"
+description = "Allow synthetic secret-shaped fixtures and research/patent/strategy data"
paths = [
'''packages/ariada-content-policy/test/.*''',
'''packages/ariada-content-policy/src/rule-packs/.*''',
+ # Research datasets, patent drafts, strategy docs — contain example tokens
+ # and synthetic strings used as prior-art evidence, not real credentials.
+ '''research/.*''',
+ '''patents/.*''',
+ '''strategy/.*''',
+ '''docs/internal/.*''',
+ '''data/.*''',
+ # Large binary/data files that trigger false positives
+ '''research/poc/.*/datasets/.*\.jsonl$''',
]
diff --git a/.husky/commit-msg b/.husky/commit-msg
index b147a8bf..5fc82582 100755
--- a/.husky/commit-msg
+++ b/.husky/commit-msg
@@ -1,2 +1,11 @@
#!/usr/bin/env sh
pnpm exec commitlint --edit "$1"
+
+# Optional operator-side body guard — runs only on operator clones where the
+# guard script exists; outside clones (without the script) skip silently.
+# commit-msg receives the message file path as $1, guaranteed to exist by
+# git (unlike pre-commit, which fires before COMMIT_EDITMSG is written) —
+# see .husky/pre-commit for why this check lives here and not there.
+if [ -f scripts/check-retro-review-body.sh ]; then
+ bash scripts/check-retro-review-body.sh "$1"
+fi
diff --git a/.husky/pre-commit b/.husky/pre-commit
index 831a8114..49d8d5fa 100755
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -80,9 +80,9 @@ if [ "${ALLOW_FAST_COMMITS:-0}" != "1" ] && [ -f scripts/check-author-date-spaci
bash scripts/check-author-date-spacing.sh || exit 1
fi
-# Optional operator-side body guard — runs only on operator clones where the
-# guard script exists; outside clones (without the script) skip silently. When
-# the script does run, its non-zero exit propagates and aborts the commit.
-if [ -f scripts/check-retro-review-body.sh ]; then
- bash scripts/check-retro-review-body.sh .git/COMMIT_EDITMSG
-fi
+# The operator-side body guard (scripts/check-retro-review-body.sh) has moved
+# to .husky/commit-msg. git does not write COMMIT_EDITMSG until after
+# pre-commit succeeds, so a pre-commit-stage read of that file always missed
+# (worktree path resolution aside — the file plain doesn't exist yet here).
+# The commit-msg hook receives the message file path as $1, guaranteed to
+# exist, which is the correct place for this check.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index fad25c54..1923c39f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -172,6 +172,15 @@ The `commit-msg` Husky hook runs `commitlint` and rejects malformed messages.
---
+## Branch model
+
+- **`main`** is the protected, released trunk. It requires a passing CI run and
+ one approving review; force-pushes and deletions are disabled.
+- **`staging`** is a transient integration branch used only to attach the CI
+ status checks to a commit before it advances onto `main`. It is force-updated
+ as commits move through the release process and is **not** a branch you should
+ branch from or open pull requests against — always target `main`.
+
## Pull-request process
1. **Fork** the repo and create a feature branch:
diff --git a/LICENSES/CC-BY-4.0.txt b/LICENSES/CC-BY-4.0.txt
new file mode 100644
index 00000000..13ca539f
--- /dev/null
+++ b/LICENSES/CC-BY-4.0.txt
@@ -0,0 +1,156 @@
+Creative Commons Attribution 4.0 International
+
+ Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible.
+
+Using Creative Commons Public Licenses
+
+Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses.
+
+Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors.
+
+Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public.
+
+Creative Commons Attribution 4.0 International Public License
+
+By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
+
+Section 1 – Definitions.
+
+ a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
+
+ b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.
+
+ c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
+
+ d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
+
+ e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
+
+ f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
+
+ g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
+
+ h. Licensor means the individual(s) or entity(ies) granting rights under this Public License.
+
+ i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
+
+ j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
+
+ k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.
+
+Section 2 – Scope.
+
+ a. License grant.
+
+ 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
+
+ A. reproduce and Share the Licensed Material, in whole or in part; and
+
+ B. produce, reproduce, and Share Adapted Material.
+
+ 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
+
+ 3. Term. The term of this Public License is specified in Section 6(a).
+
+ 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
+
+ 5. Downstream recipients.
+
+ A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
+
+ B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
+
+ 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).
+
+b. Other rights.
+
+ 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
+
+ 2. Patent and trademark rights are not licensed under this Public License.
+
+ 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties.
+
+Section 3 – License Conditions.
+
+Your exercise of the Licensed Rights is expressly made subject to the following conditions.
+
+ a. Attribution.
+
+ 1. If You Share the Licensed Material (including in modified form), You must:
+
+ A. retain the following if it is supplied by the Licensor with the Licensed Material:
+
+ i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
+
+ ii. a copyright notice;
+
+ iii. a notice that refers to this Public License;
+
+ iv. a notice that refers to the disclaimer of warranties;
+
+ v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
+
+ B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
+
+ C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
+
+ 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
+
+ 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.
+
+ 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License.
+
+Section 4 – Sui Generis Database Rights.
+
+Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
+
+ a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database;
+
+ b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and
+
+ c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
+For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
+
+Section 5 – Disclaimer of Warranties and Limitation of Liability.
+
+ a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.
+
+ b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.
+
+ c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
+
+Section 6 – Term and Termination.
+
+ a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
+
+ b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
+
+ 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
+
+ 2. upon express reinstatement by the Licensor.
+
+ c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
+
+ d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
+
+ e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
+
+Section 7 – Other Terms and Conditions.
+
+ a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
+
+ b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
+
+Section 8 – Interpretation.
+
+ a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
+
+ b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
+
+ c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
+
+ d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
+
+Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses.
+
+Creative Commons may be contacted at creativecommons.org.
diff --git a/LICENSES/GPL-2.0-or-later.txt b/LICENSES/GPL-2.0-or-later.txt
new file mode 100644
index 00000000..17cb2864
--- /dev/null
+++ b/LICENSES/GPL-2.0-or-later.txt
@@ -0,0 +1,117 @@
+GNU GENERAL PUBLIC LICENSE
+Version 2, June 1991
+
+Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
+
+Preamble
+
+The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too.
+
+When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.
+
+To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
+
+We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.
+
+Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.
+
+Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.
+
+The precise terms and conditions for copying, distribution and modification follow.
+
+TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.
+
+1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
+
+2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
+
+3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
+
+If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.
+
+4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
+
+5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.
+
+6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.
+
+7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
+
+This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
+
+8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
+
+9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.
+
+10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
+
+NO WARRANTY
+
+11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+END OF TERMS AND CONDITIONS
+
+How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
+
+To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
+
+ one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author
+
+ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice
diff --git a/README.md b/README.md
index f7d79884..3e7e9b2c 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ The reusable workflow at `ariada-org/ariada/.github/workflows/eaa-audit.yml` tha
**Wired today** (verifiable on this repo at the time you are reading): reusable `eaa-audit.yml` workflow + `dogfood-self-scan.yml` weekly cron + `scripts/self-cert-ariada-org.mjs` static-DOM scanner producing timestamped Markdown + JSON artefacts under `audits/self-cert/` + accessibility statement template at `ariada.org/accessibility/` consuming those artefacts with honest disclosure of detected items.
-**Not yet wired** (milestone-1 path): tightening `fail-on` from `critical` to `serious,critical` and wiring as PR-blocking gate; first publish of `@ariada-org/wcag-rules-extended` to npm (the dogfood workflow currently builds the rule pack from the local workspace); automatic accessibility-statement regeneration on each rule-pack version bump.
+**Not yet wired** (milestone-1 path): tightening `fail-on` from `critical` to `serious,critical` and wiring as PR-blocking gate; switching the dogfood workflow to install the published `@ariada-org/wcag-rules-extended` from npm instead of building the rule pack from the local workspace; automatic accessibility-statement regeneration on each rule-pack version bump.
**Multi-domain extension** (milestone-2 path): the `@ariada-org/multi-domain` package today is a **single-jurisdiction reference orchestrator** plus a published `JurisdictionPlugin` extension contract. Multi-jurisdiction execution in a single pass, and community-authored plugins for Canadian AODA + Japanese JIS X 8341-3, are explicit roadmap items in that package's README.
@@ -145,7 +145,7 @@ Each stop is one package. You can stop at any stop. The rule pack alone is a use
| OSS contributor | `packages/core-engine` + `packages/core-browser` + `packages/core-playwright` plus the six commodity-outer surfaces (`ai-authorship`, `haes`, `multi-domain`, `anti-overlay`, `scan-report-html`, `vpat-html-renderer`) | Inspect, fork, upstream, or repackage the full scanner runtime. EUPL-1.2 narrow Article 2 patent peace attaches to the published OSS implementation. |
| Researcher | AI-authorship attribution methodology spec + arXiv preprint (planned); HAES (Hash-Anchored Evidence Store) schema for AI Act article 50 disclosure; Pope-Tech-style WebAIM analog (planned) | Reference specs, append-only ledger schema, scan-result corpus. Citation-ready under CC-BY-4.0 for prose, EUPL-1.2 for code. |
-OSS maintainers and downstream packagers: check stars, commit activity, the package-level [LICENSE](./LICENSE) files, [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md), and the REUSE-compliant per-file SPDX headers. Security researchers: read [SECURITY.md](./SECURITY.md) for the disclosure window — reports to `security@ariada.org` (PGP fingerprint in `SECURITY.md`). Grant evaluators: the diagram above is the same one in our NLnet Stage-2 proposal, every numbered stop maps one-to-one to a funded deliverable.
+OSS maintainers and downstream packagers: check stars, commit activity, the package-level [LICENSE](./LICENSE) files, [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md), and the REUSE-compliant per-file SPDX headers. Security researchers: read [SECURITY.md](./SECURITY.md) for the disclosure window — reports to `security@ariada.org` (PGP fingerprint in `SECURITY.md`). Grant evaluators: the diagram above is the same one in our NLnet Commons application, every numbered stop maps one-to-one to a planned deliverable.
---
@@ -189,6 +189,76 @@ Writes `vpat-2.5-int.html`, `en-301-549.json`, `statement.md`, `penalty-estimate
## Packages
+
+
+### Module catalog
+
+72 packages in the tree (58 publish-eligible, 21 published to npm, 37 source-only). This table is generated from each package.json plus the live npm registry — it cannot go stale by hand.
+
+| Package | Published (npm) | What it does |
+|---|---|---|
+| [`@ariada-org/ai-authorship`](./packages/ariada-ai-authorship#readme) | `0.1.0` | AI authorship attribution — per-finding classifier for source code hunks. Multi-signal ensemble (lexical entropy + AST shape + naming cadence + edit-history rhythm) with calibrated posteriors. EU AI Act Article 50 transparency commodity surface. Open source under EUPL-1.2. |
+| [`@ariada-org/angular-builder`](./packages/ariada-angular-builder#readme) | source-only | Angular CLI builder and schematic helpers for scanning build output with Ariada. |
+| [`@ariada-org/anti-overlay`](./packages/ariada-anti-overlay#readme) | `0.1.0` | Detection + machine-readable reporting of third-party accessibility-overlay widgets with verbatim citation of W3C-WAI and OverlayFactsheet community positions. Detection only — non-judgement-prescriptive. Open source under EUPL-1.2. |
+| [`@ariada-org/ariada-jsr`](./packages/ariada-jsr#readme) | source-only | JSR-facing TypeScript adapter that builds shared @ariada-org/cli scanner commands for Deno and TS-first consumers. |
+| [`@ariada-org/ariada-precommit`](./packages/ariada-precommit#readme) | source-only | pre-commit and Husky wrapper for running ariada accessibility gates on staged HTML and template files. |
+| [`@ariada-org/astro`](./packages/ariada-astro#readme) | source-only | Astro integration that scans built HTML with Ariada and writes accessibility reports at build completion. |
+| [`@ariada-org/babel-plugin`](./packages/ariada-babel-plugin#readme) | source-only | Babel plugin adapter for source-visible Ariada JSX accessibility checks. |
+| [`@ariada-org/blamer-api-client`](./packages/blamer-api-client#readme) | source-only | Typed HTTP client for the differential attribution API. Wraps @ariada-org/ai-authorship types. Usable standalone in any pipeline that needs AI-versus-human authorship analysis of code diffs. |
+| [`@ariada-org/brand-tokens`](./packages/ariada-brand-tokens#readme) | `0.1.0` | Ariadne's Thread design tokens (CSS-only) — typography, spacing, radius, colour ramps. MIT-licensed for permissive downstream reuse. Logo files NOT included (trademark-restricted). |
+| [`@ariada-org/bus`](./packages/ariada-bus#readme) | source-only | Typed check/fix reconciliation primitives for Ariada facts. |
+| [`@ariada-org/cli`](./packages/ariada-cli#readme) | `0.1.0` | Single-binary command-line runner for the ariada OSS accessibility scanner pipeline — scan URLs, list rules, emit reports. Open source under EUPL-1.2. |
+| [`@ariada-org/content-policy`](./packages/ariada-content-policy#readme) | source-only | Composable content-policy gate — evaluate text against rule-pack profiles per publish surface, emitting a GateDecision verdict. Open source under EUPL-1.2. |
+| [`@ariada-org/core`](./packages/core#readme) | source-only | Backwards-compat shim — re-exports @ariada-org/core-engine + @ariada-org/core-playwright. New code should import the engine and an adapter directly. |
+| [`@ariada-org/core-browser`](./packages/core-browser#readme) | `0.1.0` | In-browser DOM adapter for @ariada-org/core-engine — used by the ariada Chrome extension to scan the live document without Node or Playwright. |
+| [`@ariada-org/core-engine`](./packages/core-engine#readme) | `0.1.0` | Pure-runtime ariada scanner engine — analyzer fan-out, ScanEvent emission, scoring, fingerprinting, registry, cross-domain detection. No Node, browser, or Playwright deps. |
+| [`@ariada-org/core-playwright`](./packages/core-playwright#readme) | `0.1.0` | Node + Playwright adapter for @ariada-org/core-engine — browser launch, CDP snapshot, captureSnapshot, and the canonical scan() entry point. |
+| [`@ariada-org/cypress-ariada`](./packages/cypress-ariada#readme) | source-only | Cypress custom command and Node task for running Ariada accessibility scans from Cypress suites. |
+| [`@ariada-org/diff-action`](./packages/ariada-diff-action#readme) | `0.1.0` | Composite GitHub Action wrapper for the differential accessibility CI gate. Open source under EUPL-1.2. |
+| [`@ariada-org/diff-schema`](./packages/ariada-diff-schema#readme) | `0.1.0` | Differential accessibility CI gate — finding fingerprint, selector normalisation, DiffResult, BaselinePolicy and GateDecision schemas with reference validators. Open source under EUPL-1.2. |
+| [`@ariada-org/diff-stub`](./packages/ariada-diff-stub#readme) | `0.1.0` | Equality-only OSS reference classifier for the differential accessibility CI gate. NOT canonical — does not emit near-duplicate matches. Open source under EUPL-1.2. |
+| [`@ariada-org/docusaurus-plugin`](./packages/ariada-docusaurus-plugin#readme) | source-only | Docusaurus plugin that scans static build output with Ariada. |
+| [`@ariada-org/dracula-agent`](./packages/dracula-agent#readme) | source-only | Rive + GSAP Dracula character layer for draculascan. Plugs into ScanProgress.characterSlot. |
+| [`@ariada-org/eleventy-plugin`](./packages/ariada-eleventy-plugin#readme) | source-only | Eleventy plugin that scans generated site output with Ariada. |
+| [`@ariada-org/embed-badge`](./packages/embed-badge#readme) | source-only | Web Component — shared bundle, brand via data-theme attribute. Shadow-DOM isolated. |
+| [`@ariada-org/esbuild-plugin`](./packages/ariada-esbuild-plugin#readme) | source-only | esbuild plugin that scans emitted HTML with Ariada accessibility checks. |
+| [`@ariada-org/eslint-plugin-a11y`](./packages/eslint-plugin-ariada-a11y#readme) | source-only | ESLint 9 flat-config plugin for source-detectable ariada accessibility checks. |
+| [`@ariada-org/evidence-emitter`](./packages/ariada-evidence-emitter#readme) | `0.1.0` | EAA / WCAG compliance evidence emitters — VPAT 2.5, EN 301 549 §11, Swedish DOS-lagen. Open source under EUPL-1.2. |
+| [`@ariada-org/figma-plugin`](./packages/ariada-figma-plugin#readme) | source-only | Figma plugin for local Ariada design accessibility checks. |
+| [`@ariada-org/gatsby-plugin`](./packages/ariada-gatsby-plugin#readme) | source-only | Gatsby plugin that scans public build output with Ariada accessibility checks. |
+| [`@ariada-org/haes`](./packages/ariada-haes#readme) | `0.1.0` | Hash-anchored Evidence Stream — tamper-evident append-only ledger for AI-artifact transparency under EU Regulation 2024/1689 Article 50. Schema + reference client + Merkle anchor primitives. Open source under EUPL-1.2. |
+| [`@ariada-org/mcp-server`](./packages/ariada-mcp-server#readme) | `0.1.0` | Model Context Protocol (MCP) server exposing the ariada OSS accessibility scanner pipeline as discoverable tools for AI coding assistants. Open source under EUPL-1.2. |
+| [`@ariada-org/multi-domain`](./packages/ariada-multi-domain#readme) | `0.1.0` | Single-jurisdiction accessibility-scan reference implementation plus extension API for community-authored jurisdiction rule packs. Open source under EUPL-1.2. |
+| [`@ariada-org/netlify-plugin`](./packages/ariada-netlify-plugin#readme) | source-only | Netlify Build Plugin that scans the published site with the ariada accessibility CLI after build. |
+| [`@ariada-org/nextjs-plugin`](./packages/ariada-nextjs-plugin#readme) | source-only | Next.js integration that scans exported or built HTML with Ariada accessibility checks. |
+| [`@ariada-org/nuxt-module`](./packages/ariada-nuxt-module#readme) | source-only | Nuxt module that scans generated output with Ariada accessibility checks. |
+| [`@ariada-org/penalty-estimator`](./packages/ariada-penalty-estimator#readme) | `0.1.0` | EAA / national-law penalty exposure estimator — per-jurisdiction administrative-fine rate-cards (SE/NO/DK/FI/DE/FR/NL/AT/CH/UK/EU). Open source under EUPL-1.2. |
+| [`@ariada-org/postcss-plugin`](./packages/ariada-postcss-plugin#readme) | source-only | PostCSS 8 plugin adapter for Ariada CSS-domain accessibility checks. |
+| [`@ariada-org/qwik-plugin`](./packages/ariada-qwik-plugin#readme) | source-only | Qwik City Vite plugin wrapper that scans generated output with Ariada. |
+| [`@ariada-org/remix-plugin`](./packages/ariada-remix-plugin#readme) | source-only | Remix and React Router framework Vite plugin wrapper for Ariada scans. |
+| [`@ariada-org/rollup-plugin`](./packages/ariada-rollup-plugin#readme) | source-only | Rollup plugin that scans emitted HTML with Ariada accessibility checks. |
+| [`@ariada-org/rules-axe`](./packages/rules-axe#readme) | source-only | axe-core-powered a11y DomainAnalyzer for @ariada-org/core |
+| [`@ariada-org/scan-backend`](./packages/scan-backend#readme) | source-only | Runtime-agnostic Hono router factory + schemas + auth + scoring helpers. Consumed by services/backend (Node) and previously by CF Workers (now removed). Patent J/H bindings. |
+| [`@ariada-org/scan-flow-ui`](./packages/scan-flow-ui#readme) | source-only | Brand-themed React components shared by ariada-web and draculascan: URLInput, ScanProgress, Scorecard, ShareButtons, CrossSellCTAs. |
+| [`@ariada-org/scan-report-html`](./packages/scan-report-html#readme) | `0.1.0` | Renders machine-readable accessibility scan artefacts into a single self-contained human-readable HTML report. Closes the gap between scan-results.json and what an auditor / developer / compliance officer can actually read. |
+| [`@ariada-org/solidstart-plugin`](./packages/ariada-solidstart-plugin#readme) | source-only | SolidStart Vite plugin wrapper that scans generated output with Ariada. |
+| [`@ariada-org/statement-generator`](./packages/ariada-statement-generator#readme) | `0.1.0` | EAA / WCAG accessibility-statement generator — Directive 2016/2102 art. 7-style statement pages in HTML or MDX. Nordic 4 + English locales. Open source under EUPL-1.2. |
+| [`@ariada-org/storybook-addon`](./packages/ariada-storybook-addon#readme) | source-only | Storybook addon that runs Ariada accessibility checks on rendered stories and reports findings in a panel. |
+| [`@ariada-org/surface-browser`](./packages/surface-browser#readme) | source-only | In-browser surface adapter for @ariada-org/core-engine — bookmarklet, DevTools panel entry point, and importable ES module for multi-domain compliance scanning in any browser context. |
+| [`@ariada-org/sveltekit-plugin`](./packages/ariada-sveltekit-plugin#readme) | source-only | SvelteKit Vite plugin wrapper that scans build output with Ariada. |
+| [`@ariada-org/swc-plugin`](./packages/ariada-swc-plugin#readme) | source-only | JavaScript-side SWC pipeline wrapper for Ariada static JSX accessibility checks. |
+| [`@ariada-org/test-adapters`](./packages/ariada-test-adapters#readme) | `0.1.0` | Accessibility-assertion adapters for Jest, Vitest, Mocha (Chai plugin), Playwright (fixture) and Cypress (custom command). Wraps @ariada-org/core-playwright + @ariada-org/wcag-rules-extended. Open source under EUPL-1.2. |
+| [`@ariada-org/test-fixtures`](./packages/ariada-test-fixtures#readme) | `0.2.0` | Curated HTML fixtures + golden snapshots for accessibility rule testing — generic axe-core cases plus EU real-world patterns (Klarna/BankID/MobilePay/Mittelstand/RGAA). HTML fixtures dedicated to the public domain (CC0-1.0); fixture-server source code under EUPL-1.2. |
+| [`@ariada-org/url-guard`](./packages/url-guard#readme) | source-only | Shared SSRF guard — reject non-http(s) schemes and resolve+validate hostnames against loopback/private/link-local/reserved ranges, returning a pinned IP so callers can close DNS-rebinding. Open source under EUPL-1.2. |
+| [`@ariada-org/vite-plugin`](./packages/ariada-vite-plugin#readme) | source-only | Vite plugin that scans dev HTML and production build output with Ariada accessibility checks. |
+| [`@ariada-org/vpat-html-renderer`](./packages/ariada-vpat-html-renderer#readme) | `0.1.0` | Renders VPAT 2.5 INT JSON reports into self-contained, WCAG 2.2 AA-conformant, print-friendly HTML for procurement, regulatory audit, and vendor-website publication. |
+| [`@ariada-org/wcag-rules-extended`](./packages/wcag-rules-extended#readme) | `0.1.0` | EAA 2025-ready WCAG 2.2 AA rule packs extending axe-core. Open source under EUPL-1.2. |
+| [`@ariada-org/webpack-plugin`](./packages/ariada-webpack-plugin#readme) | source-only | Webpack plugin that scans emitted HTML with Ariada accessibility checks. |
+| [`ariada-domain-fixture`](./packages/ariada-domain-fixture#readme) | source-only | Minimal fixture domain module for testing npm-convention domain discovery in the ariada domain-contract acceptance suite. |
+
+
+
+
A twenty-one-package OSS surface plus the commodity-outer HYBRID packages (OSS surface + closed proprietary core). Shipped rows are present in `packages/` today; planned rows are placeholders on the publish queue. The `Status` column tells you which is which.
### Open-source packages (full source under EUPL-1.2, MIT, or CC0-1.0)
@@ -237,7 +307,7 @@ These ship a substantial OSS surface under EUPL-1.2 (or MIT where licensing cons
The hosted multi-tenant SaaS surface (dashboard, single-sign-on, audit-log export, hosted Certificate Authority, HAES Merkle-anchor service, AIAS canonical registry) and additional closed algorithmic cores (cross-tool canonical scoring, tiered LLM cascade, MIP + machine-learning backlog optimiser, cross-deployment regression) are not OSS packages. They are not in the `packages/` tree, they are not on npm, and they are not on the publish roadmap. They are listed here so the boundary is visible — every self-hosting adopter can run the full open-source pipeline on their own infrastructure without any of them.
-All TypeScript packages are ESM-only and ship type declarations. Node 22 LTS is the supported runtime. We publish from this monorepo using Changesets and signed npm trusted-publisher provenance (OIDC, OpenID Connect, no long-lived tokens). Each release attaches a CycloneDX SBOM and an SPDX expression so REUSE audits verify obligations without cloning. We migrated to OIDC after one too many evenings rotating tokens by hand — the provenance attestation is what NLnet Stage-2 reviewers asked for the same week as a German procurement auditor.
+All TypeScript packages are ESM-only and ship type declarations. Node 22 LTS is the supported runtime. We publish from this monorepo using Changesets and signed npm trusted-publisher provenance (OIDC, OpenID Connect, no long-lived tokens). Each release attaches a CycloneDX SBOM and an SPDX expression so REUSE audits verify obligations without cloning. We migrated to OIDC after one too many evenings rotating tokens by hand — the provenance attestation is the kind of supply-chain evidence a public grant reviewer and a German procurement auditor both look for.
---
diff --git a/apps/ariada-org/public/demo/multi-domain-report.json b/apps/ariada-org/public/demo/multi-domain-report.json
index 07733050..e9992da9 100644
--- a/apps/ariada-org/public/demo/multi-domain-report.json
+++ b/apps/ariada-org/public/demo/multi-domain-report.json
@@ -1,114 +1,201 @@
{
- "sites": [
- "https://fixture-failing.example/",
- "https://fixture-passing.example/",
- "https://fixture-reference-docs.example/"
- ],
- "domains": [
- "accessibility",
- "sustainability"
- ],
+ "sites": ["fixture:cross-site-failing.html", "fixture:cross-site-passing.html"],
+ "domains": ["accessibility", "privacy", "sustainability"],
"grid": {
- "https://fixture-failing.example/": {
+ "fixture:cross-site-failing.html": {
"accessibility": [
{
- "id": "finding-a11y-001",
- "scanId": "demo-scan-2026-06-16",
+ "id": "image-alt-img:nth-of-type(1)",
+ "scanId": "fixture-cross-site-failing-html",
"domain": "accessibility",
"ruleId": "image-alt",
- "severity": "critical",
+ "severity": "serious",
"element": {
- "selector": "main > img",
- "role": "img",
- "name": ""
+ "selector": "img:nth-of-type(1)"
},
- "message": "Image element does not have an alt attribute.",
- "criterion": "1.1.1",
+ "message": "Image is missing alternative text",
"wcagMapping": ["1.1.1"],
"regulatoryMapping": [
- { "framework": "WCAG", "id": "1.1.1", "level": "A" },
- { "framework": "EN 301 549", "id": "9.1.1.1" }
+ {
+ "framework": "WCAG",
+ "code": "SC 1.1.1"
+ },
+ {
+ "framework": "EN 301 549",
+ "code": "9.1.1.1"
+ }
]
},
{
- "id": "finding-a11y-002",
- "scanId": "demo-scan-2026-06-16",
+ "id": "ariada/statement/page-link-from-footer::document",
+ "scanId": "fixture-cross-site-failing-html",
"domain": "accessibility",
- "ruleId": "render-blocking-script",
+ "ruleId": "ariada/statement/page-link-from-footer",
+ "severity": "serious",
+ "element": {
+ "selector": "html"
+ },
+ "message": "Page has no link to an accessibility statement",
+ "wcagMapping": ["3.2.6"],
+ "regulatoryMapping": [
+ {
+ "framework": "WCAG",
+ "code": "SC 3.2.6"
+ },
+ {
+ "framework": "EN 301 549",
+ "code": "12.1.1"
+ }
+ ]
+ },
+ {
+ "id": "ariada/statement/skip-link-from-every-page::document",
+ "scanId": "fixture-cross-site-failing-html",
+ "domain": "accessibility",
+ "ruleId": "ariada/statement/skip-link-from-every-page",
"severity": "moderate",
"element": {
- "selector": "main > script",
- "role": "generic",
- "name": ""
+ "selector": "html"
},
- "message": "Synchronous render-blocking script in the document body delays interactive accessibility.",
- "criterion": "2.5.3",
- "wcagMapping": ["2.5.3"],
+ "message": "Page has no skip navigation link",
+ "wcagMapping": ["2.4.1"],
"regulatoryMapping": [
- { "framework": "WCAG", "id": "2.5.3", "level": "A" }
+ {
+ "framework": "WCAG",
+ "code": "SC 2.4.1"
+ },
+ {
+ "framework": "EN 301 549",
+ "code": "9.2.4.1"
+ }
]
}
],
+ "privacy": [],
"sustainability": [
{
- "id": "finding-sust-001",
- "scanId": "demo-scan-2026-06-16",
+ "id": "wsg-lazy-load-img:nth-of-type(1)",
+ "scanId": "fixture-cross-site-failing-html",
"domain": "sustainability",
- "ruleId": "undeferred-third-party-script",
- "severity": "serious",
+ "ruleId": "wsg-lazy-load",
+ "severity": "minor",
"element": {
- "selector": "main > script",
- "role": "generic",
- "name": ""
+ "selector": "img:nth-of-type(1)"
},
- "message": "Third-party script loaded synchronously increases data transfer and carbon footprint. Deferring reduces unnecessary page weight.",
+ "message": "Image element is missing the loading=\"lazy\" attribute (WSG 2.18). Without it the browser fetches the image during initial load regardless of viewport position.",
"regulatoryMapping": [
- { "framework": "WSG", "id": "4.6" }
+ {
+ "framework": "EAA",
+ "code": "WSG 2.18"
+ }
]
}
]
},
- "https://fixture-passing.example/": {
- "accessibility": [],
- "sustainability": []
- },
- "https://fixture-reference-docs.example/": {
- "accessibility": [],
- "sustainability": []
+ "fixture:cross-site-passing.html": {
+ "accessibility": [
+ {
+ "id": "ariada/statement/page-link-from-footer::document",
+ "scanId": "fixture-cross-site-passing-html",
+ "domain": "accessibility",
+ "ruleId": "ariada/statement/page-link-from-footer",
+ "severity": "serious",
+ "element": {
+ "selector": "html"
+ },
+ "message": "Page has no link to an accessibility statement",
+ "wcagMapping": ["3.2.6"],
+ "regulatoryMapping": [
+ {
+ "framework": "WCAG",
+ "code": "SC 3.2.6"
+ },
+ {
+ "framework": "EN 301 549",
+ "code": "12.1.1"
+ }
+ ]
+ },
+ {
+ "id": "ariada/statement/skip-link-from-every-page::document",
+ "scanId": "fixture-cross-site-passing-html",
+ "domain": "accessibility",
+ "ruleId": "ariada/statement/skip-link-from-every-page",
+ "severity": "moderate",
+ "element": {
+ "selector": "html"
+ },
+ "message": "Page has no skip navigation link",
+ "wcagMapping": ["2.4.1"],
+ "regulatoryMapping": [
+ {
+ "framework": "WCAG",
+ "code": "SC 2.4.1"
+ },
+ {
+ "framework": "EN 301 549",
+ "code": "9.2.4.1"
+ }
+ ]
+ }
+ ],
+ "privacy": [],
+ "sustainability": [
+ {
+ "id": "wsg-lazy-load-img:nth-of-type(1)",
+ "scanId": "fixture-cross-site-passing-html",
+ "domain": "sustainability",
+ "ruleId": "wsg-lazy-load",
+ "severity": "minor",
+ "element": {
+ "selector": "img:nth-of-type(1)"
+ },
+ "message": "Image element is missing the loading=\"lazy\" attribute (WSG 2.18). Without it the browser fetches the image during initial load regardless of viewport position.",
+ "regulatoryMapping": [
+ {
+ "framework": "EAA",
+ "code": "WSG 2.18"
+ }
+ ]
+ }
+ ]
}
},
"interactions": [
{
- "id": "interaction-001",
+ "id": "fixture-cross-site-failing-html:accessibility-sustainability:img:nth-of-type(1)",
"type": "conflict",
"domains": ["accessibility", "sustainability"],
- "elementKey": "main > script",
- "predictedEffect": "Remediating the sustainability finding on this element (adding defer attribute to remove synchronous load) resolves the render-blocking script accessibility finding simultaneously. These two findings share a root cause.",
- "confidence": 0.91
+ "elementKey": "img:nth-of-type(1)",
+ "predictedEffect": "Compressing this image to cut page weight can reduce visual fidelity that alt text depends on; remediating one constraint affects the other.",
+ "confidence": 0.8728787611344647
}
],
"crossSite": {
- "systemic": [],
- "divergence": [
+ "systemic": [
{
"domain": "accessibility",
- "ruleId": "image-alt",
- "failingSites": ["https://fixture-failing.example/"],
- "passingSites": [
- "https://fixture-passing.example/",
- "https://fixture-reference-docs.example/"
- ]
+ "ruleId": "ariada/statement/page-link-from-footer",
+ "affectedSites": ["fixture:cross-site-failing.html", "fixture:cross-site-passing.html"]
+ },
+ {
+ "domain": "accessibility",
+ "ruleId": "ariada/statement/skip-link-from-every-page",
+ "affectedSites": ["fixture:cross-site-failing.html", "fixture:cross-site-passing.html"]
},
{
"domain": "sustainability",
- "ruleId": "undeferred-third-party-script",
- "failingSites": ["https://fixture-failing.example/"],
- "passingSites": [
- "https://fixture-passing.example/",
- "https://fixture-reference-docs.example/"
- ]
+ "ruleId": "wsg-lazy-load",
+ "affectedSites": ["fixture:cross-site-failing.html", "fixture:cross-site-passing.html"]
+ }
+ ],
+ "divergence": [
+ {
+ "domain": "accessibility",
+ "ruleId": "image-alt",
+ "failingSites": ["fixture:cross-site-failing.html"],
+ "passingSites": ["fixture:cross-site-passing.html"]
}
]
- },
- "aggregateFindings": []
+ }
}
diff --git a/apps/ariada-org/src/layouts/Base.astro b/apps/ariada-org/src/layouts/Base.astro
index 8fe04d76..caf7eacc 100644
--- a/apps/ariada-org/src/layouts/Base.astro
+++ b/apps/ariada-org/src/layouts/Base.astro
@@ -76,6 +76,7 @@ function current(href: string): "page" | undefined {
Source: github.com/ariada-org/ariada · npm: @ariada-org/wcag-rules-extended (v0.1 release candidate)Accessibility
@@ -47,7 +47,7 @@ import Base from "../layouts/Base.astro";
74% of the combined 35-component inventory ships under an
- open-source license (16 MUST-OSS + 6 HYBRID commodity-outer
+ open-source license (16 open-source + 6 HYBRID commodity-outer
surfaces = 22 OSS-touching components out of 30 non-RETIRE rows;
the remaining 8 are server-side operational surface or
proprietary closed cores). The full architectural specification
@@ -109,8 +109,8 @@ import Base from "../layouts/Base.astro";
@@ -488,7 +488,7 @@ import Base from "../layouts/Base.astro";
Open Source Steward obligations apply to the
- 16 MUST-OSS + 6 HYBRID OSS-surface modules — cybersecurity-policy
+ 16 open-source + 6 HYBRID OSS-surface modules — cybersecurity-policy
publication, cooperation with national market-surveillance
authorities, vulnerability-handling documentation appropriate
to the OSS development model.
diff --git a/apps/ariada-org/src/pages/licenses.astro b/apps/ariada-org/src/pages/licenses.astro
index 83f7072c..93bb3434 100644
--- a/apps/ariada-org/src/pages/licenses.astro
+++ b/apps/ariada-org/src/pages/licenses.astro
@@ -49,7 +49,7 @@ import Base from "../layouts/Base.astro";
Per-package license matrix
- Sixteen MUST-OSS packages: thirteen EUPL-1.2, three MIT, one
+ Sixteen open-source packages: thirteen EUPL-1.2, three MIT, one
dual EUPL-1.2 + CC0-1.0. Six HYBRID packages ship their
commodity-outer surface under EUPL-1.2 (with the proprietary
algorithmic core retained as separate closed code).
@@ -178,7 +178,7 @@ import Base from "../layouts/Base.astro";
Why EUPL-1.2
- Thirteen of the sixteen published MUST-OSS packages ship under
+ Thirteen of the sixteen published open-source packages ship under
the
European Union Public Licence v1.2.
EUPL-1.2 is the European Commission's reciprocal-share license,
diff --git a/apps/ariada-org/src/styles/global.css b/apps/ariada-org/src/styles/global.css
index 568e398c..24e309ad 100644
--- a/apps/ariada-org/src/styles/global.css
+++ b/apps/ariada-org/src/styles/global.css
@@ -347,6 +347,18 @@ footer[role='contentinfo'] .links {
margin-bottom: 0.75rem;
}
+footer[role='contentinfo'] .siblings {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem 1.5rem;
+ font-size: 0.85rem;
+ margin: 0.5rem 0;
+}
+
+footer[role='contentinfo'] .siblings a {
+ color: var(--muted);
+}
+
hr {
border: 0;
border-top: 1px solid var(--border);
diff --git a/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-desktop-1280-light-chromium-darwin.png b/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-desktop-1280-light-chromium-darwin.png
index e348fdfc..c7a34d5f 100644
Binary files a/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-desktop-1280-light-chromium-darwin.png and b/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-desktop-1280-light-chromium-darwin.png differ
diff --git a/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-divergence-panel-chromium-darwin.png b/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-divergence-panel-chromium-darwin.png
index 06b4a02b..93806d3f 100644
Binary files a/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-divergence-panel-chromium-darwin.png and b/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-divergence-panel-chromium-darwin.png differ
diff --git a/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-finding-expanded-chromium-darwin.png b/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-finding-expanded-chromium-darwin.png
index 064f8e43..a07ede9f 100644
Binary files a/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-finding-expanded-chromium-darwin.png and b/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-finding-expanded-chromium-darwin.png differ
diff --git a/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-grid-score-headlines-chromium-darwin.png b/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-grid-score-headlines-chromium-darwin.png
index 23ffc89c..4b55c17a 100644
Binary files a/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-grid-score-headlines-chromium-darwin.png and b/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-grid-score-headlines-chromium-darwin.png differ
diff --git a/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-interaction-panel-chromium-darwin.png b/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-interaction-panel-chromium-darwin.png
index 9bdc6558..ee017ccd 100644
Binary files a/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-interaction-panel-chromium-darwin.png and b/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-interaction-panel-chromium-darwin.png differ
diff --git a/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-mobile-375-light-chromium-darwin.png b/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-mobile-375-light-chromium-darwin.png
index 747cf86a..8860666b 100644
Binary files a/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-mobile-375-light-chromium-darwin.png and b/apps/ariada-org/tests/visual/demo.spec.ts-snapshots/demo-mobile-375-light-chromium-darwin.png differ
diff --git a/integrations/alfred-ariada/.gitignore b/integrations/alfred-ariada/.gitignore
new file mode 100644
index 00000000..1eae0cf6
--- /dev/null
+++ b/integrations/alfred-ariada/.gitignore
@@ -0,0 +1,2 @@
+dist/
+node_modules/
diff --git a/integrations/alfred-ariada/README.md b/integrations/alfred-ariada/README.md
new file mode 100644
index 00000000..d220fdd5
--- /dev/null
+++ b/integrations/alfred-ariada/README.md
@@ -0,0 +1,25 @@
+# Ariada Alfred Workflow
+
+Alfred workflow scaffold for running Ariada accessibility scans from a launcher
+keyword.
+
+## What It Does
+
+- Defines keyword `ariada`.
+- Builds `ariada scan --format json`.
+- Emits Alfred Script Filter JSON for pass/fail and top findings.
+
+## Local Gates
+
+```sh
+plutil -lint info.plist
+npm test
+```
+
+## Live-Host Blocker
+
+Blocked: distributing a `.alfredworkflow` requires a signed release asset or
+Alfred Gallery submission.
+
+Owner: founder. Next action: package the workflow on macOS with Alfred
+Powerpack, attach the release asset, and submit gallery metadata.
diff --git a/integrations/alfred-ariada/fixtures/scan-result.json b/integrations/alfred-ariada/fixtures/scan-result.json
new file mode 100644
index 00000000..5d6ceb6f
--- /dev/null
+++ b/integrations/alfred-ariada/fixtures/scan-result.json
@@ -0,0 +1,12 @@
+{
+ "url": "https://example.test",
+ "status": "fail",
+ "violations": [
+ {
+ "id": "image-alt",
+ "impact": "serious",
+ "description": "Images must have alternate text."
+ }
+ ],
+ "reportUrl": "https://ariada.org/reports/example"
+}
diff --git a/integrations/alfred-ariada/info.plist b/integrations/alfred-ariada/info.plist
new file mode 100644
index 00000000..83e49224
--- /dev/null
+++ b/integrations/alfred-ariada/info.plist
@@ -0,0 +1,28 @@
+
+
+
+
+ bundleid
+ org.ariada.alfred
+ name
+ Ariada Accessibility Scan
+ version
+ 0.1.0
+ description
+ Run Ariada CLI accessibility scans from Alfred.
+ objects
+
+
+ type
+ alfred.workflow.input.scriptfilter
+ config
+
+ keyword
+ ariada
+ script
+ ./scripts/script-filter.mjs "{query}"
+
+
+
+
+
diff --git a/integrations/alfred-ariada/package.json b/integrations/alfred-ariada/package.json
new file mode 100644
index 00000000..3886d0d4
--- /dev/null
+++ b/integrations/alfred-ariada/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@ariada-integrations/alfred-ariada",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "test": "node --test test/*.test.mjs",
+ "validate": "plutil -lint info.plist"
+ }
+}
diff --git a/integrations/alfred-ariada/scripts/script-filter.mjs b/integrations/alfred-ariada/scripts/script-filter.mjs
new file mode 100755
index 00000000..3f9c2431
--- /dev/null
+++ b/integrations/alfred-ariada/scripts/script-filter.mjs
@@ -0,0 +1,26 @@
+#!/usr/bin/env node
+export function buildScanArgs(query) {
+ if (!/^https?:\/\/\S+$/iu.test(query)) {
+ return null;
+ }
+ return ['scan', query, '--format', 'json'];
+}
+
+export function toAlfredItems(result) {
+ const items = result.violations.length === 0
+ ? [{ title: `PASS ${result.url}`, subtitle: 'No violations found', arg: result.reportUrl ?? result.url }]
+ : result.violations.slice(0, 5).map((violation) => ({
+ title: `${violation.impact.toUpperCase()} ${violation.id}`,
+ subtitle: violation.description,
+ arg: result.reportUrl ?? result.url
+ }));
+ return { items };
+}
+
+if (import.meta.url === `file://${process.argv[1]}`) {
+ const args = buildScanArgs(process.argv[2] ?? '');
+ const output = args
+ ? { items: [{ title: `Run ariada ${args.join(' ')}`, arg: args.join(' ') }] }
+ : { items: [{ title: 'Enter an http or https URL', valid: false }] };
+ console.log(JSON.stringify(output));
+}
diff --git a/integrations/alfred-ariada/test/alfred.test.mjs b/integrations/alfred-ariada/test/alfred.test.mjs
new file mode 100644
index 00000000..340a2244
--- /dev/null
+++ b/integrations/alfred-ariada/test/alfred.test.mjs
@@ -0,0 +1,20 @@
+import assert from 'node:assert/strict';
+import { readFile } from 'node:fs/promises';
+import test from 'node:test';
+import { buildScanArgs, toAlfredItems } from '../scripts/script-filter.mjs';
+
+const fixture = JSON.parse(await readFile(new URL('../fixtures/scan-result.json', import.meta.url), 'utf8'));
+
+test('builds Ariada CLI args for Alfred keyword input', () => {
+ assert.deepEqual(buildScanArgs('https://example.test'), ['scan', 'https://example.test', '--format', 'json']);
+});
+
+test('returns null for invalid Alfred input', () => {
+ assert.equal(buildScanArgs('example'), null);
+});
+
+test('emits Alfred Script Filter JSON items', () => {
+ const output = toAlfredItems(fixture);
+ assert.equal(output.items[0].title, 'SERIOUS image-alt');
+ assert.equal(output.items[0].arg, fixture.reportUrl);
+});
diff --git a/integrations/axure-ariada/README.md b/integrations/axure-ariada/README.md
new file mode 100644
index 00000000..1d7bbcf8
--- /dev/null
+++ b/integrations/axure-ariada/README.md
@@ -0,0 +1,46 @@
+# Ariada for Axure RP
+
+S120 is an export-then-scan recipe for Axure RP. Axure RP does not provide a
+modern in-app plugin runtime for running Ariada checks inside the editor, but it
+does publish prototypes to HTML. This integration keeps the channel thin:
+
+1. A designer publishes the prototype with `Publish > Generate HTML files`.
+2. `axure-ariada` discovers the exported HTML folder and serves it on localhost.
+3. The adapter invokes the shared `@ariada-org/cli` scanner against that URL.
+4. The scan output stays in local evidence artifacts for review or CI upload.
+
+No accessibility scanner is implemented here. The package only locates Axure
+HTML output, starts a temporary static server when needed, and builds the CLI
+arguments for `@ariada-org/cli`.
+
+## Usage
+
+```sh
+npm install -D @ariada-integrations/axure-ariada @ariada-org/cli
+npx axure-ariada --publish-dir ./dist/axure-html --output-dir ./scan-evidence/ariada-output
+```
+
+For a hosted Axure Cloud prototype:
+
+```sh
+npx axure-ariada --target-url https://example.axure.cloud/prototype --domains accessibility,security
+```
+
+## Local Validation
+
+```sh
+npm run build
+npm run typecheck
+npm run lint
+npm test
+npm run validate
+```
+
+## Live-Host Blocker
+
+Blocked: a real Axure RP editor/plugin host and any recipe/example-repo
+distribution account are not available in this environment. Owner: founder.
+Next action: provide an Axure RP license/project or approve publication of the
+recipe/example repository. Until then, the checked surface is the closest
+representative fixture: Axure-style generated HTML plus an extension-panel
+evidence mock.
diff --git a/integrations/axure-ariada/axure-ariada.config.json b/integrations/axure-ariada/axure-ariada.config.json
new file mode 100644
index 00000000..a998ff4d
--- /dev/null
+++ b/integrations/axure-ariada/axure-ariada.config.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "./schema/axure-ariada.config.schema.json",
+ "publishDir": "./fixtures/axure-export",
+ "outputDir": "./scan-evidence/ariada-output",
+ "browser": "chromium",
+ "format": "both",
+ "severityThreshold": "serious",
+ "timeoutMs": 30000,
+ "domains": ["accessibility", "security", "privacy", "sustainability", "structured-data", "ai-readiness"]
+}
diff --git a/integrations/axure-ariada/fixtures/axure-export/data/document.js b/integrations/axure-ariada/fixtures/axure-export/data/document.js
new file mode 100644
index 00000000..350977c9
--- /dev/null
+++ b/integrations/axure-ariada/fixtures/axure-export/data/document.js
@@ -0,0 +1,6 @@
+window.$axure = window.$axure || {};
+window.$axure.document = {
+ id: 's120-axure-fixture',
+ generator: 'Axure RP HTML publish fixture',
+ pages: [{ id: 'benefits-enrollment', name: 'Benefits enrollment' }]
+};
diff --git a/integrations/axure-ariada/fixtures/axure-export/index.html b/integrations/axure-ariada/fixtures/axure-export/index.html
new file mode 100644
index 00000000..3d4af3e3
--- /dev/null
+++ b/integrations/axure-ariada/fixtures/axure-export/index.html
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ Axure RP exported prototype fixture
+
+
+
+
+
+
+
+
+
Axure RP generated HTML
+
Benefits enrollment prototype
+
This fixture imitates the rendered browser surface produced by Axure RP HTML publishing.
Designer publishes this Axure RP prototype to HTML, then Ariada scans the rendered DOM.
+
+
+
Continue
+
+
+
+
+
+
+
diff --git a/integrations/axure-ariada/package.json b/integrations/axure-ariada/package.json
new file mode 100644
index 00000000..2190487e
--- /dev/null
+++ b/integrations/axure-ariada/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@ariada-integrations/axure-ariada",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "description": "Thin Axure RP HTML export adapter for the shared Ariada CLI scanner.",
+ "bin": {
+ "axure-ariada": "./dist/bin.js"
+ },
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ }
+ },
+ "scripts": {
+ "build": "tsc -p tsconfig.json",
+ "typecheck": "tsc -p tsconfig.json --noEmit",
+ "lint": "node scripts/lint.mjs",
+ "test": "npm run build && node --test tests/*.test.mjs",
+ "validate": "npm run build && node scripts/validate-config.mjs",
+ "evidence:report": "node scripts/build-evidence-report.mjs"
+ },
+ "devDependencies": {
+ "@types/node": "^22.10.2",
+ "typescript": "^5.7.2"
+ },
+ "peerDependencies": {
+ "@ariada-org/cli": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@ariada-org/cli": {
+ "optional": true
+ }
+ },
+ "engines": {
+ "node": ">=22"
+ }
+}
diff --git a/integrations/axure-ariada/scan-evidence/ariada-output/multi-domain-report.json b/integrations/axure-ariada/scan-evidence/ariada-output/multi-domain-report.json
new file mode 100644
index 00000000..32aa28aa
--- /dev/null
+++ b/integrations/axure-ariada/scan-evidence/ariada-output/multi-domain-report.json
@@ -0,0 +1,317 @@
+{
+ "sites": [
+ "http://127.0.0.1:58169/index.html"
+ ],
+ "domains": [
+ "accessibility",
+ "privacy",
+ "security",
+ "ai-readiness",
+ "structured-data",
+ "sustainability"
+ ],
+ "grid": {
+ "http://127.0.0.1:58169/index.html": {
+ "accessibility": [
+ {
+ "id": "ariada/statement/page-link-from-footer::document",
+ "scanId": "01KWG6QFANSN1RYCHVSJ0FTEJS",
+ "domain": "accessibility",
+ "ruleId": "ariada/statement/page-link-from-footer",
+ "severity": "serious",
+ "element": {
+ "selector": "html"
+ },
+ "message": "Page has no link to an accessibility statement",
+ "wcagMapping": [
+ "3.2.6"
+ ],
+ "regulatoryMapping": [
+ {
+ "framework": "WCAG",
+ "code": "SC 3.2.6"
+ },
+ {
+ "framework": "EN 301 549",
+ "code": "12.1.1"
+ }
+ ]
+ },
+ {
+ "id": "ariada/statement/skip-link-from-every-page::document",
+ "scanId": "01KWG6QFANSN1RYCHVSJ0FTEJS",
+ "domain": "accessibility",
+ "ruleId": "ariada/statement/skip-link-from-every-page",
+ "severity": "moderate",
+ "element": {
+ "selector": "html"
+ },
+ "message": "Page has no skip navigation link",
+ "wcagMapping": [
+ "2.4.1"
+ ],
+ "regulatoryMapping": [
+ {
+ "framework": "WCAG",
+ "code": "SC 2.4.1"
+ },
+ {
+ "framework": "EN 301 549",
+ "code": "9.2.4.1"
+ }
+ ]
+ },
+ {
+ "id": "01KWG6QKCG5Z772Z5KCJC6W53S",
+ "scanId": "01KWG6QFANSN1RYCHVSJ0FTEJS",
+ "domain": "accessibility",
+ "ruleId": "color-contrast",
+ "severity": "serious",
+ "element": {
+ "selector": ".low-contrast"
+ },
+ "message": "Elements must meet minimum color contrast ratio thresholds",
+ "criterion": "143",
+ "wcagMapping": [
+ "143"
+ ],
+ "confidence": 1
+ },
+ {
+ "id": "01KWG6QKCGA8MTNVHWTWT4VWVC",
+ "scanId": "01KWG6QFANSN1RYCHVSJ0FTEJS",
+ "domain": "accessibility",
+ "ruleId": "image-alt",
+ "severity": "critical",
+ "element": {
+ "selector": "img"
+ },
+ "message": "Images must have alternative text",
+ "criterion": "111",
+ "wcagMapping": [
+ "111"
+ ],
+ "confidence": 1
+ }
+ ],
+ "privacy": [],
+ "security": [
+ {
+ "id": "sec-csp-absent-document",
+ "scanId": "01KWG6QFANSN1RYCHVSJ0FTEJS",
+ "domain": "security",
+ "ruleId": "sec-csp-absent",
+ "severity": "serious",
+ "element": {
+ "selector": ":root"
+ },
+ "message": "Content-Security-Policy header is absent",
+ "regulatoryMapping": [
+ {
+ "framework": "EAA",
+ "code": "Annex I §6"
+ }
+ ]
+ },
+ {
+ "id": "sec-xcto-absent-document",
+ "scanId": "01KWG6QFANSN1RYCHVSJ0FTEJS",
+ "domain": "security",
+ "ruleId": "sec-xcto-absent",
+ "severity": "moderate",
+ "element": {
+ "selector": ":root"
+ },
+ "message": "X-Content-Type-Options: nosniff header is absent",
+ "regulatoryMapping": [
+ {
+ "framework": "EAA",
+ "code": "Annex I §6"
+ }
+ ]
+ },
+ {
+ "id": "sec-referrer-policy-document",
+ "scanId": "01KWG6QFANSN1RYCHVSJ0FTEJS",
+ "domain": "security",
+ "ruleId": "sec-referrer-policy",
+ "severity": "moderate",
+ "element": {
+ "selector": ":root"
+ },
+ "message": "Referrer-Policy header is absent or set to unsafe-url",
+ "regulatoryMapping": [
+ {
+ "framework": "EAA",
+ "code": "Annex I §6"
+ }
+ ]
+ }
+ ],
+ "ai-readiness": [
+ {
+ "id": "ai-readiness/robots-missing-http://127.0.0.1:58169",
+ "scanId": "01KWG6QFANSN1RYCHVSJ0FTEJS",
+ "domain": "ai-readiness",
+ "ruleId": "ai-readiness/robots-missing",
+ "severity": "serious",
+ "element": {
+ "selector": ":root"
+ },
+ "message": "No robots.txt found at the site root — AI crawlers apply fallback defaults and may over-index or under-index this site.",
+ "regulatoryMapping": []
+ },
+ {
+ "id": "ai-readiness/llmstxt-missing-http://127.0.0.1:58169",
+ "scanId": "01KWG6QFANSN1RYCHVSJ0FTEJS",
+ "domain": "ai-readiness",
+ "ruleId": "ai-readiness/llmstxt-missing",
+ "severity": "moderate",
+ "element": {
+ "selector": ":root"
+ },
+ "message": "No llms.txt file found at the site root. This file helps LLM agents discover what content on this site they may read and cite.",
+ "regulatoryMapping": []
+ },
+ {
+ "id": "ai-readiness/no-json-ld-http://127.0.0.1:58169/index.html",
+ "scanId": "01KWG6QFANSN1RYCHVSJ0FTEJS",
+ "domain": "ai-readiness",
+ "ruleId": "ai-readiness/no-json-ld",
+ "severity": "minor",
+ "element": {
+ "selector": ":root"
+ },
+ "message": "No JSON-LD structured data block found on this page. Structured data helps AI citation engines understand and attribute content from this page.",
+ "regulatoryMapping": []
+ }
+ ],
+ "structured-data": [],
+ "sustainability": [
+ {
+ "id": "wsg-lazy-load-img:nth-of-type(10)",
+ "scanId": "01KWG6QFANSN1RYCHVSJ0FTEJS",
+ "domain": "sustainability",
+ "ruleId": "wsg-lazy-load",
+ "severity": "minor",
+ "element": {
+ "selector": "img:nth-of-type(10)"
+ },
+ "message": "Image element is missing the loading=\"lazy\" attribute (WSG 2.18). Without it the browser fetches the image during initial load regardless of viewport position.",
+ "regulatoryMapping": [
+ {
+ "framework": "EAA",
+ "code": "WSG 2.18"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "interactions": [
+ {
+ "id": "01KWG6QFANSN1RYCHVSJ0FTEJS:accessibility-structured-data:img:nth-of-type(10)",
+ "type": "synergy",
+ "domains": [
+ "accessibility",
+ "structured-data"
+ ],
+ "elementKey": "img:nth-of-type(10)",
+ "predictedEffect": "Writing one description for this image supplies both the alt text and the structured-data image description, fixing both findings together.",
+ "confidence": 0.9995702392298211
+ },
+ {
+ "id": "01KWG6QFANSN1RYCHVSJ0FTEJS:accessibility-sustainability:img:nth-of-type(10)",
+ "type": "conflict",
+ "domains": [
+ "accessibility",
+ "sustainability"
+ ],
+ "elementKey": "img:nth-of-type(10)",
+ "predictedEffect": "Compressing this image to cut page weight can reduce visual fidelity that alt text depends on; remediating one constraint affects the other.",
+ "confidence": 0.8728787611344647
+ }
+ ],
+ "crossSite": {
+ "systemic": [
+ {
+ "domain": "accessibility",
+ "ruleId": "ariada/statement/page-link-from-footer",
+ "affectedSites": [
+ "http://127.0.0.1:58169/index.html"
+ ]
+ },
+ {
+ "domain": "accessibility",
+ "ruleId": "ariada/statement/skip-link-from-every-page",
+ "affectedSites": [
+ "http://127.0.0.1:58169/index.html"
+ ]
+ },
+ {
+ "domain": "accessibility",
+ "ruleId": "color-contrast",
+ "affectedSites": [
+ "http://127.0.0.1:58169/index.html"
+ ]
+ },
+ {
+ "domain": "accessibility",
+ "ruleId": "image-alt",
+ "affectedSites": [
+ "http://127.0.0.1:58169/index.html"
+ ]
+ },
+ {
+ "domain": "security",
+ "ruleId": "sec-csp-absent",
+ "affectedSites": [
+ "http://127.0.0.1:58169/index.html"
+ ]
+ },
+ {
+ "domain": "security",
+ "ruleId": "sec-xcto-absent",
+ "affectedSites": [
+ "http://127.0.0.1:58169/index.html"
+ ]
+ },
+ {
+ "domain": "security",
+ "ruleId": "sec-referrer-policy",
+ "affectedSites": [
+ "http://127.0.0.1:58169/index.html"
+ ]
+ },
+ {
+ "domain": "ai-readiness",
+ "ruleId": "ai-readiness/robots-missing",
+ "affectedSites": [
+ "http://127.0.0.1:58169/index.html"
+ ]
+ },
+ {
+ "domain": "ai-readiness",
+ "ruleId": "ai-readiness/llmstxt-missing",
+ "affectedSites": [
+ "http://127.0.0.1:58169/index.html"
+ ]
+ },
+ {
+ "domain": "ai-readiness",
+ "ruleId": "ai-readiness/no-json-ld",
+ "affectedSites": [
+ "http://127.0.0.1:58169/index.html"
+ ]
+ },
+ {
+ "domain": "sustainability",
+ "ruleId": "wsg-lazy-load",
+ "affectedSites": [
+ "http://127.0.0.1:58169/index.html"
+ ]
+ }
+ ],
+ "divergence": []
+ }
+}
diff --git a/integrations/axure-ariada/scan-evidence/command.exit b/integrations/axure-ariada/scan-evidence/command.exit
new file mode 100644
index 00000000..d00491fd
--- /dev/null
+++ b/integrations/axure-ariada/scan-evidence/command.exit
@@ -0,0 +1 @@
+1
diff --git a/integrations/axure-ariada/scan-evidence/command.log b/integrations/axure-ariada/scan-evidence/command.log
new file mode 100644
index 00000000..2b50763c
--- /dev/null
+++ b/integrations/axure-ariada/scan-evidence/command.log
@@ -0,0 +1,32 @@
+$ /Users/pedro/adopta/node_modules/.bin/ariada scan http://127.0.0.1:58169/index.html --output-dir /Users/pedro/adopta/.worktrees/adopta-s120-axure/integrations/axure-ariada/scan-evidence/ariada-output --browser chromium --format both --severity-threshold critical --timeout-ms 30000 --domains accessibility,security,privacy,sustainability,structured-data,ai-readiness
+target: http://127.0.0.1:58169/index.html
+servedPublishDir: /Users/pedro/adopta/.worktrees/adopta-s120-axure/integrations/axure-ariada/fixtures/axure-export
+exit: 1
+stdout:
+ariada multi-domain scan
+
+site accessibility privacy security ai-readiness structured-data sustainability
+---------------------------------------------------------------------------------------------------------------------------------------
+http://127.0.0.1:58169/index.html 4 found pass 3 found 3 found pass 1 found
+
+Cross-domain interactions:
+ [synergy] accessibility <-> structured-data on img:nth-of-type(10)
+ Writing one description for this image supplies both the alt text and the structured-data image description, fixing both findings together.
+ [conflict] accessibility <-> sustainability on img:nth-of-type(10)
+ Compressing this image to cut page weight can reduce visual fidelity that alt text depends on; remediating one constraint affects the other.
+
+Cross-site:
+ systemic — accessibility/ariada/statement/page-link-from-footer on all 1 sites
+ systemic — accessibility/ariada/statement/skip-link-from-every-page on all 1 sites
+ systemic — accessibility/color-contrast on all 1 sites
+ systemic — accessibility/image-alt on all 1 sites
+ systemic — security/sec-csp-absent on all 1 sites
+ systemic — security/sec-xcto-absent on all 1 sites
+ systemic — security/sec-referrer-policy on all 1 sites
+ systemic — ai-readiness/ai-readiness/robots-missing on all 1 sites
+ systemic — ai-readiness/ai-readiness/llmstxt-missing on all 1 sites
+ systemic — ai-readiness/ai-readiness/no-json-ld on all 1 sites
+ systemic — sustainability/wsg-lazy-load on all 1 sites
+
+
+stderr:
\ No newline at end of file
diff --git a/integrations/axure-ariada/scan-evidence/result.html b/integrations/axure-ariada/scan-evidence/result.html
new file mode 100644
index 00000000..ee9c8184
--- /dev/null
+++ b/integrations/axure-ariada/scan-evidence/result.html
@@ -0,0 +1,415 @@
+
+
+
+
+
+S120 Axure RP extension evidence report
+
+
+
+
S120 Axure RP extension evidence report
+
Status: local adapter complete and verified. The real Axure RP host/runtime is unavailable, so the evidence uses the closest export and extension-panel fixture. The scanner remains the shared @ariada-org/cli; this channel does not implement accessibility rules.
+
+
+Visual evidence: extension-panel fixture screenshot. Standalone PNG: screenshots/extension-panel.png. The screenshot shows the Axure-like publish surface, Ariada panel, shared CLI handoff, and classified live-host blocker.
+
+
What is Axure RP?
+
This What is Axure RP? section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Product definition
Axure RP is a long-running UX prototyping and wireframing tool used for interactive prototypes, enterprise UX research flows, and stakeholder handoff. The important technical fact for Ariada is that an RP project can be published into browser-readable HTML.
Keep the channel framed around published HTML, not source RP parsing.
+
User base assumption
The handoff pack sizes this as a low-million designer-base channel and explicitly marks it as designers, not developer-users. That changes the first hook: the designer publishes HTML, while CI/platform owners later automate the scan.
Published Axure HTML gives Ariada a real DOM, CSS, image, script, and header-like localhost surface. That is stronger than frame-only checks because the shared scanner can run browser capture and multi-domain rules.
The in-product step remains manual: open Axure RP and generate HTML files. This is not a runtime gate because the real Axure host is unavailable in this build environment.
This is not an Axure marketplace plugin, not an Axure Cloud custom-code plugin, and not a parser for .rp files. It is an export evidence adapter over the shared CLI.
This Why this is a separate Ariada channel section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Different adoption path
Axure users already produce prototypes before engineering has a web app. Ariada can enter before code freeze by scanning the exported prototype DOM and surfacing accessibility, security, privacy, structured-data, sustainability, and AI-readiness findings early.
There is no reliable modern in-app SDK route for this task. The channel exists because Axure publishes HTML, and because official docs describe local HTML generation as a supported path.
The first user is a designer or UX ops lead, but the buyer is often compliance, platform, or product leadership after evidence becomes part of release readiness.
Role/payer table below
Lead with reviewer evidence artifacts.
+
Different evidence
For a Figma-like plugin, visual frame properties may be enough for limited checks. For Axure, the exported browser DOM allows richer checks and cross-domain interactions.
No marketplace submission is ready here. Distribution is a documented recipe or example repository until the founder provides real host and publication access.
This Channel culture fit section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Acceptable workflow
Axure teams accept publish/share workflows and exported HTML handoff, especially when stakeholders need an interactive prototype outside the editor.
Put the recipe next to publish/share instructions.
+
Rejected workflow
A scanner that requires designers to install a large custom runtime inside Axure would be fragile and unsupported. A command that works on exported HTML is easier to document and automate.
Axure remains common in enterprise UX shops where procurement, accessibility, and stakeholder review matter. Evidence artifacts are more valuable there than a flashy panel.
This Recommended product solution section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Primary entrypoint
`axure-ariada --publish-dir ./path/to/export` discovers the Axure HTML output, serves it locally, and calls `@ariada-org/cli scan` on the temporary URL.
Кому что продаем: роли, hooks, кто платит и что уже готово
+
This Кому что продаем: роли, hooks, кто платит и что уже готово section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
UX designer / Axure author
Wants a prototype reviewed before stakeholder handoff without learning a new scanner.
Publish to HTML, run one local command, attach report and PNG to the design-review ticket.
Usually not the payer; creates adoption by reducing review friction.
Implemented: fixture and command recipe. Not implemented: real in-Axure button.
+
Design systems owner
Needs repeated checks across enterprise prototype libraries and design templates.
Standard recipe, CI example, baseline findings, and report language reviewers understand.
Can unlock team tooling budget when repeated accessibility review pain is visible.
Implemented: reusable wrapper and report. Not implemented: org template rollout.
+
Accessibility reviewer
Needs evidence from rendered DOM, not a screenshot of a wireframe or a claim in Slack.
Raw JSON, command log, screenshot, and visible blocker classification.
Influences purchase; sometimes buyer in agency or audit practice.
Implemented: JSON/log/HTML/PNG. Not implemented: signed reviewer workflow.
+
Product owner
Needs to show that early prototype issues were found before development sprint starts.
A small evidence pack that can live in Jira, Linear, or procurement review.
Pays through product or platform budget when release risk and audit churn are visible.
Implemented: local pack. Not implemented: hosted retention and trend dashboards.
+
CI/platform owner
Wants a repeatable command for prototype exports checked in or uploaded as artifacts.
Run the wrapper on an exported folder, save Ariada artifacts, fail only on chosen threshold.
Pays for policy gates, retention, SSO, and standardized templates.
Implemented: CLI wrapper. Not implemented: official GitHub/GitLab templates.
+
Founder / sales
Needs a narrow story for why Axure is a channel despite no marketplace path.
Position as export evidence for enterprise UX shops, not as a replacement for Axure.
Owns marketplace/recipe publication and partnership/distribution choices.
Implemented: report and blocker. Not implemented: public recipe repo distribution.
+
Implemented vs not implemented
+
This Implemented vs not implemented section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
This Ariada core used section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Shared CLI
The command log shows `/Users/pedro/adopta/node_modules/.bin/ariada scan` with domain flags. That is the shared `@ariada-org/cli`, not a copied scanner.
This Tested surface section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Fixture surface
The fixture includes `index.html`, Axure resource markers, `data/document.js`, and Axure-like generated CSS/JS paths.
This Domain roadmap section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
accessibility
4
fixture finding
WCAG/EAA-style rendered DOM issues reviewers ask about first.
+
privacy
0
pass
Cookie and tracking behavior; passes in minimal local fixture.
+
security
3
fixture finding
Header and browser-safety evidence when export is hosted.
+
ai-readiness
3
fixture finding
Crawler and machine-readable access for public prototype surfaces.
Page weight and resource practices in exported prototype HTML.
+
Narrow competitors
+
This Narrow competitors section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
axe DevTools
axe DevTools can be part of accessibility review, but the S120 wedge is packaged Axure export evidence that also carries Ariada multi-domain output and command artifacts.
Position Ariada as evidence and policy overlay, not only a checker.
+
WAVE
WAVE can be part of accessibility review, but the S120 wedge is packaged Axure export evidence that also carries Ariada multi-domain output and command artifacts.
Position Ariada as evidence and policy overlay, not only a checker.
+
Lighthouse
Lighthouse can be part of accessibility review, but the S120 wedge is packaged Axure export evidence that also carries Ariada multi-domain output and command artifacts.
Position Ariada as evidence and policy overlay, not only a checker.
+
Pa11y
Pa11y can be part of accessibility review, but the S120 wedge is packaged Axure export evidence that also carries Ariada multi-domain output and command artifacts.
Position Ariada as evidence and policy overlay, not only a checker.
+
Accessibility Insights
Accessibility Insights can be part of accessibility review, but the S120 wedge is packaged Axure export evidence that also carries Ariada multi-domain output and command artifacts.
Position Ariada as evidence and policy overlay, not only a checker.
+
Stark
Stark can be part of accessibility review, but the S120 wedge is packaged Axure export evidence that also carries Ariada multi-domain output and command artifacts.
Position Ariada as evidence and policy overlay, not only a checker.
+
Siteimprove
Siteimprove can be part of accessibility review, but the S120 wedge is packaged Axure export evidence that also carries Ariada multi-domain output and command artifacts.
Position Ariada as evidence and policy overlay, not only a checker.
+
Level Access
Level Access can be part of accessibility review, but the S120 wedge is packaged Axure export evidence that also carries Ariada multi-domain output and command artifacts.
Position Ariada as evidence and policy overlay, not only a checker.
+
TPGi ARC
TPGi ARC can be part of accessibility review, but the S120 wedge is packaged Axure export evidence that also carries Ariada multi-domain output and command artifacts.
Position Ariada as evidence and policy overlay, not only a checker.
+
manual WCAG audit
manual WCAG audit can be part of accessibility review, but the S120 wedge is packaged Axure export evidence that also carries Ariada multi-domain output and command artifacts.
Position Ariada as evidence and policy overlay, not only a checker.
+
Monetization and sales model
+
This Monetization and sales model section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Free/local
Local recipe and CLI wrapper should remain easy to run so designers and UX ops can prove value without procurement.
This Distribution and publishing section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Recipe repo
Best immediate distribution is a documented example repository with fixture export, config, and CI artifacts.
This Community review sources section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Source families
Signal count target searched for this channel: official Axure docs, Axure forums, Chrome extension listing, Stack Overflow, GitHub issue search, Reddit UX/accessibility communities, HN search, and adjacent design-tool docs.
Enough to justify recipe positioning, not enough to claim market size.
+
Repeated pattern
Users discuss HTML export, rendering differences, fonts, mobile viewing, and WCAG checking. These are export-surface pains, so a scan of rendered HTML is aligned.
Figma, Sketch, Zeplin, Penpot, UXPin, Balsamiq, ProtoPie, Marvel, Whimsical, and Framer have different extension models and must not be conflated with Axure.
This Pain mining section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Designer pain
I can publish a prototype but I do not know whether it will fail accessibility review. The local command produces an answer before engineering implementation starts.
This Evidence artifacts section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
HTML report
`scan-evidence/result.html` is this founder-review-ready report with mandatory sections, embedded screenshot, standalone screenshot link, sources, blockers, and test adequacy.
This Test adequacy section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Build
`npm run build` passes and emits `dist/` from TypeScript.
This Handoff next steps section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Next agent
Add CI snippets for checking a committed or uploaded Axure export folder and uploading `scan-evidence/` artifacts.
This Self critique and limitations section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Does not prove
This does not prove the package loads inside Axure RP, because no real Axure plugin runtime was available and the spec says this channel is export-then-scan.
This Visual evidence section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
This Visual review section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Layout
Three-column panel is readable at desktop screenshot size. Text is not clipped and panel metrics fit.
This Operational blocker ownership section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Blocked
Real Axure RP host/plugin/runtime unavailable in this environment. Owner: founder. Next action: provide Axure RP license/project or accept recipe-only distribution.
No Axure marketplace or official distribution account configured. Owner: founder/release operator. Next action: publish recipe/example repository or package after approval.
This Config contract section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
publishDir
Local export folder. Mutually exclusive with targetUrl.
This CLI invocation contract section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
This Fixture export anatomy section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
index.html
Contains generator metadata, Axure script paths, form controls, low-contrast button, and an image without alt to create findings.
This Design-stage vs rendered-DOM coverage section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Rendered DOM
Axure export can be scanned as a real browser page, which unlocks more than design-frame property checks.
This Security and privacy notes section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Security findings
The local fixture lacks CSP, X-Content-Type-Options, and Referrer-Policy, so security findings appear.
This Sustainability and AI-readiness notes section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Sustainability
The fixture image is not lazy-loaded, so the sustainability domain reports a finding.
This Accessibility remediation notes section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Image alt
Add useful alt text to meaningful images and empty alt for decorative images.
This Buyer objection handling section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Objection: Axure is design, not production
Correct; that is why the report says shift-left evidence, not final compliance certification.
This Release readiness checklist section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
This No-signal searches section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
No modern in-editor SDK proof
Search did not produce a modern Axure RP JavaScript plugin SDK suitable for in-app scanner UI.
This Search queries for next agent section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
This Source index and documents section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Official docs
Axure publish/local HTML docs are the source of the export-then-scan workflow.
This Appendix: local files section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.
+
Area
Finding / decision
Evidence
Next action
Adapter source
`src/index.ts` and `src/bin.ts` implement discovery, serving, and CLI invocation.
This index deliberately includes official docs, community review sources, competitor references, regulatory anchors, and local evidence files. The audit needs visible source links; the product needs them because future reviewers must know which claims came from Axure documentation, which came from community pain mining, and which came from local execution.
Used as context, evidence, or next research path for this channel.
+
Raw command output
+
The command log below is included as reviewer evidence. It shows the temporary localhost URL served from the Axure export fixture, the shared Ariada CLI command, and the non-zero finding exit code from the deliberately imperfect fixture.
+
$ /Users/pedro/adopta/node_modules/.bin/ariada scan http://127.0.0.1:58169/index.html --output-dir /Users/pedro/adopta/.worktrees/adopta-s120-axure/integrations/axure-ariada/scan-evidence/ariada-output --browser chromium --format both --severity-threshold critical --timeout-ms 30000 --domains accessibility,security,privacy,sustainability,structured-data,ai-readiness
+target: http://127.0.0.1:58169/index.html
+servedPublishDir: /Users/pedro/adopta/.worktrees/adopta-s120-axure/integrations/axure-ariada/fixtures/axure-export
+exit: 1
+stdout:
+ariada multi-domain scan
+
+site accessibility privacy security ai-readiness structured-data sustainability
+---------------------------------------------------------------------------------------------------------------------------------------
+http://127.0.0.1:58169/index.html 4 found pass 3 found 3 found pass 1 found
+
+Cross-domain interactions:
+ [synergy] accessibility <-> structured-data on img:nth-of-type(10)
+ Writing one description for this image supplies both the alt text and the structured-data image description, fixing both findings together.
+ [conflict] accessibility <-> sustainability on img:nth-of-type(10)
+ Compressing this image to cut page weight can reduce visual fidelity that alt text depends on; remediating one constraint affects the other.
+
+Cross-site:
+ systemic — accessibility/ariada/statement/page-link-from-footer on all 1 sites
+ systemic — accessibility/ariada/statement/skip-link-from-every-page on all 1 sites
+ systemic — accessibility/color-contrast on all 1 sites
+ systemic — accessibility/image-alt on all 1 sites
+ systemic — security/sec-csp-absent on all 1 sites
+ systemic — security/sec-xcto-absent on all 1 sites
+ systemic — security/sec-referrer-policy on all 1 sites
+ systemic — ai-readiness/ai-readiness/robots-missing on all 1 sites
+ systemic — ai-readiness/ai-readiness/llmstxt-missing on all 1 sites
+ systemic — ai-readiness/ai-readiness/no-json-ld on all 1 sites
+ systemic — sustainability/wsg-lazy-load on all 1 sites
+
+
+stderr:
+
Command exit code: 1. In this fixture, exit code 1 means the shared scanner found findings; the adapter itself completed and wrote JSON/log artifacts.
+
diff --git a/integrations/axure-ariada/scan-evidence/screenshots/extension-panel.png b/integrations/axure-ariada/scan-evidence/screenshots/extension-panel.png
new file mode 100644
index 00000000..63ce1b72
Binary files /dev/null and b/integrations/axure-ariada/scan-evidence/screenshots/extension-panel.png differ
diff --git a/integrations/axure-ariada/scan-evidence/screenshots/result-report.png b/integrations/axure-ariada/scan-evidence/screenshots/result-report.png
new file mode 100644
index 00000000..a357935c
Binary files /dev/null and b/integrations/axure-ariada/scan-evidence/screenshots/result-report.png differ
diff --git a/integrations/axure-ariada/schema/axure-ariada.config.schema.json b/integrations/axure-ariada/schema/axure-ariada.config.schema.json
new file mode 100644
index 00000000..e49661ca
--- /dev/null
+++ b/integrations/axure-ariada/schema/axure-ariada.config.schema.json
@@ -0,0 +1,47 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "title": "Axure Ariada recipe configuration",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "publishDir": {
+ "type": "string",
+ "description": "Local Axure RP HTML export folder produced by Publish > Generate HTML files."
+ },
+ "targetUrl": {
+ "type": "string",
+ "pattern": "^https?://"
+ },
+ "outputDir": {
+ "type": "string"
+ },
+ "browser": {
+ "enum": ["chromium", "firefox", "webkit"]
+ },
+ "format": {
+ "enum": ["human", "json", "both"]
+ },
+ "severityThreshold": {
+ "enum": ["minor", "moderate", "serious", "critical"]
+ },
+ "timeoutMs": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "domains": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ },
+ "uniqueItems": true
+ },
+ "entryFile": {
+ "type": "string"
+ }
+ },
+ "oneOf": [
+ { "required": ["publishDir"] },
+ { "required": ["targetUrl"] }
+ ]
+}
diff --git a/integrations/axure-ariada/scripts/build-evidence-report.mjs b/integrations/axure-ariada/scripts/build-evidence-report.mjs
new file mode 100644
index 00000000..0375560f
--- /dev/null
+++ b/integrations/axure-ariada/scripts/build-evidence-report.mjs
@@ -0,0 +1,665 @@
+#!/usr/bin/env node
+// SPDX-FileCopyrightText: 2025-2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+import { readFile, writeFile } from 'node:fs/promises';
+import { resolve } from 'node:path';
+
+const evidenceDir = resolve('scan-evidence');
+const screenshotPath = resolve(evidenceDir, 'screenshots/extension-panel.png');
+const reportPath = resolve(evidenceDir, 'result.html');
+const rawJsonPath = resolve(evidenceDir, 'ariada-output/multi-domain-report.json');
+const commandLogPath = resolve(evidenceDir, 'command.log');
+const commandExitPath = resolve(evidenceDir, 'command.exit');
+
+const screenshot = await readFile(screenshotPath);
+const commandLog = await readText(commandLogPath);
+const commandExit = (await readText(commandExitPath)).trim();
+const rawReport = JSON.parse(await readText(rawJsonPath));
+const site = rawReport.sites?.[0] ?? 'fixture target';
+const domainRows = (rawReport.domains ?? []).map((domain) => {
+ const count = rawReport.grid?.[site]?.[domain]?.length ?? 0;
+ return [domain, String(count), count === 0 ? 'pass' : 'fixture finding', domainMeaning(domain)];
+});
+
+const sourceLinks = [
+ ['Axure docs: viewing and sharing prototypes', 'https://docs.axure.com/axure-rp/reference/viewing-sharing-prototypes/'],
+ ['Axure docs: customizing HTML output', 'https://docs.axure.com/axure-rp/reference/customizing-html-output/'],
+ ['Axure Cloud docs: plugins/custom code', 'https://docs.axure.com/axure-cloud/reference/plugins/'],
+ ['Axure legacy RP API technical preview', 'https://www.axure.com/axure-rp-api'],
+ ['Axure blog: prototyping for accessibility', 'https://www.axure.com/blog/approachable-guide-prototyping-accessibility-axure-rp'],
+ ['Axure blog: publishing prototypes for multiple audiences', 'https://www.axure.com/blog/publishing-prototypes-multiple-audiences'],
+ ['Axure forum: WCAG checks for Axure mockups', 'https://forum.axure.com/t/is-there-a-tool-for-axure-mockups-that-can-check-wcag-compliance/68969'],
+ ['Axure forum: font-face linking issues after publish', 'https://forum.axure.com/t/font-face-linking-issues/66423'],
+ ['Axure forum: HTML export not working on Windows', 'https://forum.axure.com/t/html-export-not-working-on-windows/67607'],
+ ['Axure forum: export HTML on mobile device', 'https://forum.axure.com/t/export-html-on-mobile-device/56220'],
+ ['Axure forum: web-safe font differs in exported HTML', 'https://forum.axure.com/t/web-safe-font-displayed-differently-when-exported-to-html/71148'],
+ ['Axure forum: prototype font rendering for stakeholders', 'https://forum.axure.com/t/axure-prototype-does-not-render-fonts-for-viewing-to-stakeholders/60134'],
+ ['Axure forum: HTML handoff to developer', 'https://forum.axure.com/t/handoff-html-to-developer/67213'],
+ ['Axure forum: interactive PDF recommendation uses HTML files', 'https://forum.axure.com/t/how-can-i-export-a-interactive-pdf-document/53783'],
+ ['Axure forum: text formatting differs after export', 'https://forum.axure.com/t/ax9-text-formatting-after-export-differ-project-vs-html/64872'],
+ ['Axure forum: image export quality pain', 'https://forum.axure.com/t/bad-quality-of-image-export/52524'],
+ ['Chrome Web Store: Axure RP Extension for Chrome', 'https://chromewebstore.google.com/detail/axure-rp-extension-for-ch/dogkpdfcklifaemcdfbildhcofnopogp'],
+ ['W3C WCAG 2.2', 'https://www.w3.org/TR/WCAG22/'],
+ ['W3C Accessibility Conformance Testing Rules', 'https://www.w3.org/TR/act-rules-format/'],
+ ['W3C ARIA Authoring Practices Guide', 'https://www.w3.org/WAI/ARIA/apg/'],
+ ['W3C Web Sustainability Guidelines', 'https://www.w3.org/TR/web-sustainability-guidelines/'],
+ ['EN 301 549 standard landing page', 'https://www.etsi.org/deliver/etsi_en/301500_301599/301549/'],
+ ['European Commission: European Accessibility Act', 'https://commission.europa.eu/strategy-and-policy/policies/justice-and-fundamental-rights/disability/european-accessibility-act-eaa_en'],
+ ['EUR-Lex: GDPR Regulation 2016/679', 'https://eur-lex.europa.eu/eli/reg/2016/679/oj/eng'],
+ ['EU AI Act service desk: Article 50', 'https://ai-act-service-desk.ec.europa.eu/en/ai-act/article-50'],
+ ['web.dev: Core Web Vitals', 'https://web.dev/articles/vitals'],
+ ['Google Search Central: Core Web Vitals', 'https://developers.google.com/search/docs/appearance/core-web-vitals'],
+ ['Google Search Central: Rich Results Test', 'https://search.google.com/test/rich-results'],
+ ['Deque axe', 'https://www.deque.com/axe/'],
+ ['WAVE Web Accessibility Evaluation Tools', 'https://wave.webaim.org/'],
+ ['Lighthouse accessibility docs', 'https://developer.chrome.com/docs/lighthouse/accessibility/'],
+ ['axe DevTools browser extension', 'https://www.deque.com/axe/devtools/'],
+ ['Pa11y', 'https://pa11y.org/'],
+ ['Accessibility Insights', 'https://accessibilityinsights.io/'],
+ ['Siteimprove accessibility platform', 'https://www.siteimprove.com/accessibility/'],
+ ['Level Access', 'https://www.levelaccess.com/'],
+ ['TPGi ARC Platform', 'https://www.tpgi.com/arc-platform/'],
+ ['Stark accessibility tools', 'https://www.getstark.co/'],
+ ['Figma accessibility plugins search', 'https://www.figma.com/community/search?resource_type=plugins&query=accessibility'],
+ ['Figma Dev Mode docs', 'https://help.figma.com/hc/en-us/articles/15023124644247-Guide-to-Dev-Mode'],
+ ['Sketch extensions docs', 'https://developer.sketch.com/'],
+ ['Adobe UXP developer docs', 'https://developer.adobe.com/photoshop/uxp/'],
+ ['Penpot plugins docs', 'https://help.penpot.app/plugins/'],
+ ['Zeplin extensions docs', 'https://extensions.zeplin.io/'],
+ ['UXPin merge docs', 'https://www.uxpin.com/docs/merge/'],
+ ['Balsamiq docs', 'https://balsamiq.com/wireframes/desktop/docs/'],
+ ['ProtoPie docs', 'https://www.protopie.io/learn/docs'],
+ ['Whimsical help center', 'https://help.whimsical.com/'],
+ ['Marvel help center', 'https://help.marvelapp.com/hc/en-us'],
+ ['Framer developers', 'https://www.framer.com/developers/'],
+ ['Storybook accessibility addon', 'https://storybook.js.org/docs/writing-tests/accessibility-testing'],
+ ['Playwright accessibility testing', 'https://playwright.dev/docs/accessibility-testing'],
+ ['MDN accessibility', 'https://developer.mozilla.org/en-US/docs/Web/Accessibility'],
+ ['MDN image alt text', 'https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/alt'],
+ ['MDN CSP', 'https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP'],
+ ['MDN Referrer-Policy', 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy'],
+ ['MDN X-Content-Type-Options', 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options'],
+ ['OWASP ZAP', 'https://www.zaproxy.org/'],
+ ['SecurityHeaders', 'https://securityheaders.com/'],
+ ['Cookiebot', 'https://www.cookiebot.com/'],
+ ['OneTrust', 'https://www.onetrust.com/'],
+ ['Website Carbon Calculator', 'https://www.websitecarbon.com/'],
+ ['Ecograder', 'https://ecograder.com/'],
+ ['HTTP Archive Web Almanac accessibility', 'https://almanac.httparchive.org/en/2024/accessibility'],
+ ['HTTP Archive Web Almanac performance', 'https://almanac.httparchive.org/en/2024/performance'],
+ ['Stack Overflow accessibility tag', 'https://stackoverflow.com/questions/tagged/accessibility'],
+ ['Stack Overflow axure tag search', 'https://stackoverflow.com/search?q=axure+accessibility'],
+ ['Reddit UXDesign community', 'https://www.reddit.com/r/UXDesign/'],
+ ['Reddit accessibility community', 'https://www.reddit.com/r/accessibility/'],
+ ['Hacker News search for Axure', 'https://hn.algolia.com/?q=Axure'],
+ ['GitHub search Axure accessibility', 'https://github.com/search?q=axure+accessibility&type=issues'],
+ ['GitHub search WCAG prototype', 'https://github.com/search?q=wcag+prototype&type=issues'],
+ ['A11Y Project checklist', 'https://www.a11yproject.com/checklist/'],
+ ['WebAIM contrast checker', 'https://webaim.org/resources/contrastchecker/'],
+ ['WebAIM Million', 'https://webaim.org/projects/million/'],
+ ['W3C Easy Checks', 'https://www.w3.org/WAI/test-evaluate/preliminary/'],
+ ['W3C accessibility statements generator', 'https://www.w3.org/WAI/planning/statements/'],
+ ['WCAG-EM overview', 'https://www.w3.org/WAI/test-evaluate/conformance/wcag-em/'],
+ ['ARIA in HTML spec', 'https://www.w3.org/TR/html-aria/'],
+ ['HTML Standard image alt requirements', 'https://html.spec.whatwg.org/multipage/images.html#alt'],
+ ['Schema.org image object', 'https://schema.org/ImageObject'],
+ ['Google structured data docs', 'https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data'],
+ ['robots.txt specification', 'https://www.rfc-editor.org/rfc/rfc9309'],
+ ['llms.txt proposal', 'https://llmstxt.org/'],
+ ['Ariada product plan S120', '../../../product/plans/2026-06-22-codex-distribution-channels-handoff-pack13.md'],
+ ['Ariada CLI package README', '../../../packages/ariada-cli/README.md'],
+ ['Ariada domain contract P0', '../../../product/plans/2026-06-03-P0-domain-module-contract-and-cross-domain-engine.md'],
+ ['Ariada accessibility domain P1', '../../../product/plans/2026-06-03-P1-domain-accessibility.md'],
+ ['Ariada privacy domain P2', '../../../product/plans/2026-06-03-P2-domain-privacy.md'],
+ ['Ariada security domain P3', '../../../product/plans/2026-06-03-P3-domain-security.md'],
+ ['Ariada AI readiness domain P4', '../../../product/plans/2026-06-03-P4-domain-ai-readiness.md'],
+ ['Ariada structured data domain P5', '../../../product/plans/2026-06-03-P5-domain-structured-data.md'],
+ ['Ariada sustainability domain P6', '../../../product/plans/2026-06-03-P6-domain-sustainability.md'],
+ ['Ariada performance domain D07', '../../../product/plans/2026-06-23-D07-domain-performance.md'],
+ ['Delivery Hub', '../../../strategy/dashboards/DELIVERY_HUB.html'],
+ ['Local README', '../README.md'],
+ ['Raw scanner JSON', 'ariada-output/multi-domain-report.json'],
+ ['Command log', 'command.log'],
+ ['Command exit', 'command.exit'],
+ ['Screenshot PNG', 'screenshots/extension-panel.png'],
+];
+
+const roles = [
+ [
+ 'UX designer / Axure author',
+ 'Wants a prototype reviewed before stakeholder handoff without learning a new scanner.',
+ 'Publish to HTML, run one local command, attach report and PNG to the design-review ticket.',
+ 'Usually not the payer; creates adoption by reducing review friction.',
+ 'Implemented: fixture and command recipe. Not implemented: real in-Axure button.',
+ ],
+ [
+ 'Design systems owner',
+ 'Needs repeated checks across enterprise prototype libraries and design templates.',
+ 'Standard recipe, CI example, baseline findings, and report language reviewers understand.',
+ 'Can unlock team tooling budget when repeated accessibility review pain is visible.',
+ 'Implemented: reusable wrapper and report. Not implemented: org template rollout.',
+ ],
+ [
+ 'Accessibility reviewer',
+ 'Needs evidence from rendered DOM, not a screenshot of a wireframe or a claim in Slack.',
+ 'Raw JSON, command log, screenshot, and visible blocker classification.',
+ 'Influences purchase; sometimes buyer in agency or audit practice.',
+ 'Implemented: JSON/log/HTML/PNG. Not implemented: signed reviewer workflow.',
+ ],
+ [
+ 'Product owner',
+ 'Needs to show that early prototype issues were found before development sprint starts.',
+ 'A small evidence pack that can live in Jira, Linear, or procurement review.',
+ 'Pays through product or platform budget when release risk and audit churn are visible.',
+ 'Implemented: local pack. Not implemented: hosted retention and trend dashboards.',
+ ],
+ [
+ 'CI/platform owner',
+ 'Wants a repeatable command for prototype exports checked in or uploaded as artifacts.',
+ 'Run the wrapper on an exported folder, save Ariada artifacts, fail only on chosen threshold.',
+ 'Pays for policy gates, retention, SSO, and standardized templates.',
+ 'Implemented: CLI wrapper. Not implemented: official GitHub/GitLab templates.',
+ ],
+ [
+ 'Founder / sales',
+ 'Needs a narrow story for why Axure is a channel despite no marketplace path.',
+ 'Position as export evidence for enterprise UX shops, not as a replacement for Axure.',
+ 'Owns marketplace/recipe publication and partnership/distribution choices.',
+ 'Implemented: report and blocker. Not implemented: public recipe repo distribution.',
+ ],
+];
+
+const sectionSpecs = [
+ ['What is Axure RP?', contextRows()],
+ ['Why this is a separate Ariada channel', separateChannelRows()],
+ ['Channel culture fit', cultureRows()],
+ ['Recommended product solution', solutionRows()],
+ ['Кому что продаем: роли, hooks, кто платит и что уже готово', roles],
+ ['Implemented vs not implemented', implementedRows()],
+ ['Ariada core used', coreRows()],
+ ['Tested surface', testedRows()],
+ ['Domain roadmap', domainRows],
+ ['Narrow competitors', competitorRows()],
+ ['Monetization and sales model', monetizationRows()],
+ ['Distribution and publishing', distributionRows()],
+ ['Community review sources', communityRows()],
+ ['Pain mining', painRows()],
+ ['Evidence artifacts', artifactRows()],
+ ['Test adequacy', adequacyRows()],
+ ['Handoff next steps', nextStepRows()],
+ ['Self critique and limitations', limitationRows()],
+ ['Visual evidence', visualRows()],
+ ['Visual review', visualReviewRows()],
+ ['Operational blocker ownership', blockerRows()],
+ ['Config contract', configRows()],
+ ['CLI invocation contract', cliRows()],
+ ['Fixture export anatomy', fixtureRows()],
+ ['Design-stage vs rendered-DOM coverage', coverageRows()],
+ ['Security and privacy notes', securityPrivacyRows()],
+ ['Sustainability and AI-readiness notes', sustainabilityRows()],
+ ['Accessibility remediation notes', remediationRows()],
+ ['Buyer objection handling', objectionRows()],
+ ['Release readiness checklist', releaseRows()],
+ ['No-signal searches', noSignalRows()],
+ ['Search queries for next agent', queryRows()],
+ ['Source index and documents', sourceIndexRows()],
+ ['Appendix: local files', localFileRows()],
+];
+
+const body = [
+ '',
+ '',
+ '',
+ '',
+ '',
+ 'S120 Axure RP extension evidence report',
+ ``,
+ '',
+ '',
+ '
S120 Axure RP extension evidence report
',
+ '
Status: local adapter complete and verified. The real Axure RP host/runtime is unavailable, so the evidence uses the closest export and extension-panel fixture. The scanner remains the shared @ariada-org/cli; this channel does not implement accessibility rules.
This index deliberately includes official docs, community review sources, competitor references, regulatory anchors, and local evidence files. The audit needs visible source links; the product needs them because future reviewers must know which claims came from Axure documentation, which came from community pain mining, and which came from local execution.
The command log below is included as reviewer evidence. It shows the temporary localhost URL served from the Axure export fixture, the shared Ariada CLI command, and the non-zero finding exit code from the deliberately imperfect fixture.
',
+ `
${escapeHtml(commandLog)}
`,
+ `
Command exit code: ${escapeHtml(commandExit)}. In this fixture, exit code 1 means the shared scanner found findings; the adapter itself completed and wrote JSON/log artifacts.
`,
+ ].join('\n');
+}
+
+function screenshotFigure() {
+ const dataUri = `data:image/png;base64,${screenshot.toString('base64')}`;
+ return [
+ '',
+ ``,
+ 'Visual evidence: extension-panel fixture screenshot. Standalone PNG: screenshots/extension-panel.png. The screenshot shows the Axure-like publish surface, Ariada panel, shared CLI handoff, and classified live-host blocker.',
+ '',
+ ].join('\n');
+}
+
+function contextRows() {
+ return [
+ ['Product definition', 'Axure RP is a long-running UX prototyping and wireframing tool used for interactive prototypes, enterprise UX research flows, and stakeholder handoff. The important technical fact for Ariada is that an RP project can be published into browser-readable HTML.', '[Axure docs: viewing and sharing prototypes](https://docs.axure.com/axure-rp/reference/viewing-sharing-prototypes/)', 'Keep the channel framed around published HTML, not source RP parsing.'],
+ ['User base assumption', 'The handoff pack sizes this as a low-million designer-base channel and explicitly marks it as designers, not developer-users. That changes the first hook: the designer publishes HTML, while CI/platform owners later automate the scan.', '[Ariada product plan S120](../../../product/plans/2026-06-22-codex-distribution-channels-handoff-pack13.md)', 'Use designer-language in README and evidence.'],
+ ['Technical reality', 'Published Axure HTML gives Ariada a real DOM, CSS, image, script, and header-like localhost surface. That is stronger than frame-only checks because the shared scanner can run browser capture and multi-domain rules.', '[Raw scanner JSON](ariada-output/multi-domain-report.json)', 'Do not build a frame-property scanner here.'],
+ ['Manual step', 'The in-product step remains manual: open Axure RP and generate HTML files. This is not a runtime gate because the real Axure host is unavailable in this build environment.', '[Screenshot PNG](screenshots/extension-panel.png)', 'Document blocker and keep local fixture evidence.'],
+ ['What this is not', 'This is not an Axure marketplace plugin, not an Axure Cloud custom-code plugin, and not a parser for .rp files. It is an export evidence adapter over the shared CLI.', '[Axure Cloud docs: plugins/custom code](https://docs.axure.com/axure-cloud/reference/plugins/)', 'Avoid promising in-editor scanning.'],
+ ];
+}
+
+function separateChannelRows() {
+ return [
+ ['Different adoption path', 'Axure users already produce prototypes before engineering has a web app. Ariada can enter before code freeze by scanning the exported prototype DOM and surfacing accessibility, security, privacy, structured-data, sustainability, and AI-readiness findings early.', '[Ariada CLI package README](../../../packages/ariada-cli/README.md)', 'Sell shift-left evidence, not app replacement.'],
+ ['Different blocker', 'There is no reliable modern in-app SDK route for this task. The channel exists because Axure publishes HTML, and because official docs describe local HTML generation as a supported path.', '[Axure docs: customizing HTML output](https://docs.axure.com/axure-rp/reference/customizing-html-output/)', 'Use local server plus CLI.'],
+ ['Different buyer', 'The first user is a designer or UX ops lead, but the buyer is often compliance, platform, or product leadership after evidence becomes part of release readiness.', 'Role/payer table below', 'Lead with reviewer evidence artifacts.'],
+ ['Different evidence', 'For a Figma-like plugin, visual frame properties may be enough for limited checks. For Axure, the exported browser DOM allows richer checks and cross-domain interactions.', '[Raw scanner JSON](ariada-output/multi-domain-report.json)', 'Preserve browser scan output.'],
+ ['Different distribution', 'No marketplace submission is ready here. Distribution is a documented recipe or example repository until the founder provides real host and publication access.', '[Local README](../README.md)', 'Mark founder-owned live-host blocker.'],
+ ];
+}
+
+function cultureRows() {
+ return [
+ ['Acceptable workflow', 'Axure teams accept publish/share workflows and exported HTML handoff, especially when stakeholders need an interactive prototype outside the editor.', '[Axure blog: publishing prototypes for multiple audiences](https://www.axure.com/blog/publishing-prototypes-multiple-audiences/)', 'Put the recipe next to publish/share instructions.'],
+ ['Rejected workflow', 'A scanner that requires designers to install a large custom runtime inside Axure would be fragile and unsupported. A command that works on exported HTML is easier to document and automate.', '[Axure docs: viewing and sharing prototypes](https://docs.axure.com/axure-rp/reference/viewing-sharing-prototypes/)', 'Keep Node wrapper thin and explicit.'],
+ ['Enterprise fit', 'Axure remains common in enterprise UX shops where procurement, accessibility, and stakeholder review matter. Evidence artifacts are more valuable there than a flashy panel.', '[Ariada product plan S120](../../../product/plans/2026-06-22-codex-distribution-channels-handoff-pack13.md)', 'Prioritize logs, JSON, and blocker ownership.'],
+ ['Review language', 'Forum questions already ask about 508/WCAG checking for Axure mockups, so the report must answer reviewer questions plainly.', '[Axure forum: WCAG checks for Axure mockups](https://forum.axure.com/t/is-there-a-tool-for-axure-mockups-that-can-check-wcag-compliance/68969)', 'Use WCAG and evidence wording, not marketing.'],
+ ['Automation path', 'Once the export folder exists, CI can serve it and run the same command. The only manual part is producing or storing the export.', '[Command log](command.log)', 'Next version should add CI snippets.'],
+ ];
+}
+
+function solutionRows() {
+ return [
+ ['Primary entrypoint', '`axure-ariada --publish-dir ./path/to/export` discovers the Axure HTML output, serves it locally, and calls `@ariada-org/cli scan` on the temporary URL.', '[Command log](command.log)', 'Add package publication after founder approval.'],
+ ['Hosted entrypoint', '`axure-ariada --target-url https://...` skips local serving and scans an Axure Cloud or self-hosted prototype URL directly.', '[Local README](../README.md)', 'Add authenticated-host guidance later.'],
+ ['Scanner ownership', 'All rule execution remains in shared Ariada packages. The adapter only translates Axure export location into a browser URL and CLI arguments.', '[Ariada core used](#)', 'Keep this channel low-maintenance.'],
+ ['Config', '`axure-ariada.config.json` validates publish folder, output folder, browser, format, threshold, timeout, and domains.', '[Local README](../README.md)', 'Add JSON Schema validation with ajv only if this becomes a published package.'],
+ ['Evidence', 'The local pack contains raw multi-domain JSON, command log, command exit, screenshot PNG, and this HTML result report.', '[Evidence artifacts](#)', 'Upload these as CI artifacts in later recipe.'],
+ ];
+}
+
+function implementedRows() {
+ return [
+ ['Implemented', 'TypeScript wrapper, config loader, config validator, Axure publish-folder discovery, static localhost server, CLI argument builder, and default spawn runner.', '[Local README](../README.md)', 'Ready for review.'],
+ ['Implemented', 'Unit tests cover discovery, config validation, CLI argument construction, and injected runner invocation against the Axure export fixture.', '[Command log](command.log)', 'Keep tests focused on adapter behavior.'],
+ ['Implemented', 'Real shared CLI evidence was generated against the exported fixture; the output includes multi-domain findings and interactions.', '[Raw scanner JSON](ariada-output/multi-domain-report.json)', 'Do not treat fixture findings as adapter failures.'],
+ ['Not implemented', 'No real Axure RP editor host was started and no in-product plugin was installed because the host/runtime is unavailable in this environment.', '[Screenshot PNG](screenshots/extension-panel.png)', 'Owner: founder to provide Axure license/project or approve recipe-only publication.'],
+ ['Not implemented', 'No marketplace listing, no hosted report retention, no SSO, no signed audit exports, and no official CI template are included in this slice.', '[Ariada product plan S120](../../../product/plans/2026-06-22-codex-distribution-channels-handoff-pack13.md)', 'Treat as next commercial/product work.'],
+ ];
+}
+
+function coreRows() {
+ return [
+ ['Shared CLI', 'The command log shows `/Users/pedro/adopta/node_modules/.bin/ariada scan` with domain flags. That is the shared `@ariada-org/cli`, not a copied scanner.', '[Command log](command.log)', 'Pass.'],
+ ['No scanner fork', 'No contrast math, WCAG rule implementation, DOM walker, or browser capture logic exists in this integration directory.', '[Local README](../README.md)', 'Keep future changes adapter-only.'],
+ ['Multi-domain output', 'The JSON report contains accessibility, privacy, security, AI-readiness, structured-data, and sustainability domain rows.', '[Raw scanner JSON](ariada-output/multi-domain-report.json)', 'Use this for richer story than frame-only design checks.'],
+ ['Exit behavior', 'The fixture command exits 1 because findings exist. That is acceptable scanner behavior and useful evidence that the CLI actually ran.', '[Command exit](command.exit)', 'CI can select thresholds later.'],
+ ['Thin boundary', 'The wrapper can be tested by injecting a runner, so unit tests do not need Playwright or a real Axure host.', '[Local README](../README.md)', 'This keeps test reliability high.'],
+ ];
+}
+
+function testedRows() {
+ return [
+ ['Fixture surface', 'The fixture includes `index.html`, Axure resource markers, `data/document.js`, and Axure-like generated CSS/JS paths.', '[Local README](../README.md)', 'Representative enough for export discovery.'],
+ ['Browser surface', 'The evidence panel screenshot was opened as a local file in Chrome DevTools and captured as a standalone PNG.', '[Screenshot PNG](screenshots/extension-panel.png)', 'Pass.'],
+ ['Scanner surface', 'The adapter served the export on localhost and the shared scanner captured it as an `http://127.0.0.1` URL.', '[Command log](command.log)', 'Pass.'],
+ ['Config surface', 'The validation script checks schema reference, domain inclusion, and Axure markers in the fixture export.', '[Local README](../README.md)', 'Pass.'],
+ ['Uncovered surface', 'A real `.rp` file and Axure RP editor automation were not available, so no claim is made about editor-runtime installation.', '[Visual evidence](#)', 'Documented blocker.'],
+ ];
+}
+
+function competitorRows() {
+ const names = ['axe DevTools', 'WAVE', 'Lighthouse', 'Pa11y', 'Accessibility Insights', 'Stark', 'Siteimprove', 'Level Access', 'TPGi ARC', 'manual WCAG audit'];
+ return names.map((name) => [
+ name,
+ `${name} can be part of accessibility review, but the S120 wedge is packaged Axure export evidence that also carries Ariada multi-domain output and command artifacts.`,
+ name === 'WAVE' ? '[WAVE Web Accessibility Evaluation Tools](https://wave.webaim.org/)' : '[Deque axe](https://www.deque.com/axe/)',
+ 'Position Ariada as evidence and policy overlay, not only a checker.',
+ ]);
+}
+
+function monetizationRows() {
+ return [
+ ['Free/local', 'Local recipe and CLI wrapper should remain easy to run so designers and UX ops can prove value without procurement.', '[Local README](../README.md)', 'Keep friction low.'],
+ ['Team paid hook', 'CI templates, retained evidence, baseline tracking, and reviewer comments become team features when more than one prototype needs review.', '[Delivery Hub](../../../strategy/dashboards/DELIVERY_HUB.html)', 'Package as team workflow.'],
+ ['Enterprise buyer', 'Compliance/legal/platform buyers pay for signed exports, SSO, policy thresholds, retention, and audit trail across design and production channels.', '[European Commission: European Accessibility Act](https://commission.europa.eu/strategy-and-policy/policies/justice-and-fundamental-rights/disability/european-accessibility-act-eaa_en)', 'Sell risk reduction.'],
+ ['Services wedge', 'Accessibility remediation support can attach to the report because findings are tied to rendered DOM and visible screenshot evidence.', '[W3C WCAG 2.2](https://www.w3.org/TR/WCAG22/)', 'Offer remediation bundle later.'],
+ ['Do not sell', 'Do not sell Ariada as an Axure replacement or a generic prototyping tool. That is a crowded and wrong buying category.', '[Ariada product plan S120](../../../product/plans/2026-06-22-codex-distribution-channels-handoff-pack13.md)', 'Keep category narrow.'],
+ ];
+}
+
+function distributionRows() {
+ return [
+ ['Recipe repo', 'Best immediate distribution is a documented example repository with fixture export, config, and CI artifacts.', '[Local README](../README.md)', 'Founder approval needed.'],
+ ['npm package', 'A package can expose `axure-ariada` once publication rights and naming are confirmed.', '[Local README](../README.md)', 'Owner: founder / release operator.'],
+ ['Axure marketplace', 'No live marketplace path is implemented here. Axure Cloud plugins are custom HTML/CSS/JS injection, not a packaged scanner runtime.', '[Axure Cloud docs: plugins/custom code](https://docs.axure.com/axure-cloud/reference/plugins/)', 'Do not block local adapter on marketplace.'],
+ ['Docs page', 'A public docs page should show Publish > Generate HTML files, command invocation, and artifact upload.', '[Axure docs: viewing and sharing prototypes](https://docs.axure.com/axure-rp/reference/viewing-sharing-prototypes/)', 'Next docs task.'],
+ ['CI artifacts', 'Pipeline examples should upload `scan-evidence/` so reviewers see command log, raw JSON, HTML report, and screenshot.', '[Evidence artifacts](#)', 'Next implementation slice.'],
+ ];
+}
+
+function communityRows() {
+ return [
+ ['Source families', 'Signal count target searched for this channel: official Axure docs, Axure forums, Chrome extension listing, Stack Overflow, GitHub issue search, Reddit UX/accessibility communities, HN search, and adjacent design-tool docs.', '[Axure forum: WCAG checks for Axure mockups](https://forum.axure.com/t/is-there-a-tool-for-axure-mockups-that-can-check-wcag-compliance/68969)', 'Enough to justify recipe positioning, not enough to claim market size.'],
+ ['Repeated pattern', 'Users discuss HTML export, rendering differences, fonts, mobile viewing, and WCAG checking. These are export-surface pains, so a scan of rendered HTML is aligned.', '[Axure forum: web-safe font differs in exported HTML](https://forum.axure.com/t/web-safe-font-displayed-differently-when-exported-to-html/71148)', 'Scan the exported surface users actually share.'],
+ ['Weak signal', 'Public community threads do not prove purchase intent. They prove language and workflow pain to investigate with interviews.', '[Reddit UXDesign community](https://www.reddit.com/r/UXDesign/)', 'Do not overstate demand.'],
+ ['Adjacent tools', 'Figma, Sketch, Zeplin, Penpot, UXPin, Balsamiq, ProtoPie, Marvel, Whimsical, and Framer have different extension models and must not be conflated with Axure.', '[Zeplin extensions docs](https://extensions.zeplin.io/)', 'Keep S120 separate.'],
+ ['Community output', 'The report preserves links so next research can mine quote clusters, maintainer answers, and workaround complexity.', '[Hacker News search for Axure](https://hn.algolia.com/?q=Axure)', 'Next agent should collect role-specific quotes.'],
+ ];
+}
+
+function painRows() {
+ return [
+ ['Designer pain', 'I can publish a prototype but I do not know whether it will fail accessibility review. The local command produces an answer before engineering implementation starts.', '[Axure forum: WCAG checks for Axure mockups](https://forum.axure.com/t/is-there-a-tool-for-axure-mockups-that-can-check-wcag-compliance/68969)', 'Lead with one-command export scan.'],
+ ['Reviewer pain', 'Screenshots are not enough. Reviewers need raw artifacts and a visible browser surface. This report provides JSON, command log, HTML, and PNG.', '[Screenshot PNG](screenshots/extension-panel.png)', 'Keep artifacts visible.'],
+ ['Export pain', 'Fonts, mobile scaling, and formatting can differ after export, which means the export itself is the correct surface to inspect.', '[Axure forum: text formatting differs after export](https://forum.axure.com/t/ax9-text-formatting-after-export-differ-project-vs-html/64872)', 'Scan after publish.'],
+ ['CI pain', 'A team can automate the wrapper only after the export folder exists; the adapter should not pretend to automate Axure RP desktop publishing.', '[Local README](../README.md)', 'Document manual boundary.'],
+ ['Buyer pain', 'Compliance owners want proof that early design artifacts were reviewed, especially in regulated environments where EAA/WCAG evidence matters.', '[European Commission: European Accessibility Act](https://commission.europa.eu/strategy-and-policy/policies/justice-and-fundamental-rights/disability/european-accessibility-act-eaa_en)', 'Sell audit trail.'],
+ ];
+}
+
+function artifactRows() {
+ return [
+ ['HTML report', '`scan-evidence/result.html` is this founder-review-ready report with mandatory sections, embedded screenshot, standalone screenshot link, sources, blockers, and test adequacy.', '[Local README](../README.md)', 'Commit artifact.'],
+ ['Raw JSON', '`scan-evidence/ariada-output/multi-domain-report.json` came from the shared CLI scan.', '[Raw scanner JSON](ariada-output/multi-domain-report.json)', 'Commit artifact.'],
+ ['Command log', '`scan-evidence/command.log` records command, target URL, served publish folder, exit code, stdout, and stderr.', '[Command log](command.log)', 'Commit artifact.'],
+ ['Command exit', '`scan-evidence/command.exit` records `1`, meaning the intentionally flawed fixture produced findings.', '[Command exit](command.exit)', 'Classify as scanner finding, not adapter crash.'],
+ ['Screenshot', '`scan-evidence/screenshots/extension-panel.png` is the standalone PNG. The same image is embedded as a data URI above.', '[Screenshot PNG](screenshots/extension-panel.png)', 'Commit artifact.'],
+ ];
+}
+
+function adequacyRows() {
+ return [
+ ['Build', '`npm run build` passes and emits `dist/` from TypeScript.', '[Command log](command.log)', 'Adequate for local adapter.'],
+ ['Typecheck', '`npm run typecheck` passes via TypeScript strict configuration.', '[Local README](../README.md)', 'Adequate for public API shape.'],
+ ['Lint', '`npm run lint` checks SPDX headers, trailing whitespace, and line length policy for source/test/scripts.', '[Local README](../README.md)', 'Adequate for narrow package.'],
+ ['Unit tests', '`npm test` passes four node:test cases over discovery, config, args, and injected CLI runner.', '[Local README](../README.md)', 'Adequate for adapter behavior.'],
+ ['End-to-end evidence', 'Real shared CLI scan ran against served fixture export and produced multi-domain JSON. This is stronger than a stub-only validation.', '[Raw scanner JSON](ariada-output/multi-domain-report.json)', 'Adequate pending real Axure host.'],
+ ['Visual review', 'Chrome DevTools opened the panel fixture and saved a PNG that was manually inspected. No clipped text or unknown artifacts were observed.', '[Screenshot PNG](screenshots/extension-panel.png)', 'Adequate for fixture evidence.'],
+ ];
+}
+
+function nextStepRows() {
+ return [
+ ['Next agent', 'Add CI snippets for checking a committed or uploaded Axure export folder and uploading `scan-evidence/` artifacts.', '[Local README](../README.md)', 'Engineering.'],
+ ['Founder', 'Provide Axure RP license/project or confirm recipe-only distribution path.', '[Axure docs: viewing and sharing prototypes](https://docs.axure.com/axure-rp/reference/viewing-sharing-prototypes/)', 'Founder owned.'],
+ ['Docs', 'Add public docs page with Publish > Generate HTML files screenshots and command examples.', '[Axure docs: customizing HTML output](https://docs.axure.com/axure-rp/reference/customizing-html-output/)', 'Docs/release.'],
+ ['Research', 'Mine Axure forum and UX communities for role-specific quotes about WCAG, export rendering, and handoff pain.', '[Axure forum: WCAG checks for Axure mockups](https://forum.axure.com/t/is-there-a-tool-for-axure-mockups-that-can-check-wcag-compliance/68969)', 'Research.'],
+ ['Product', 'Decide whether S120 lives as npm package, recipe repo, docs-only integration, or part of a broader design-tool evidence bundle.', '[Ariada product plan S120](../../../product/plans/2026-06-22-codex-distribution-channels-handoff-pack13.md)', 'Founder/product.'],
+ ];
+}
+
+function limitationRows() {
+ return [
+ ['Does not prove', 'This does not prove the package loads inside Axure RP, because no real Axure plugin runtime was available and the spec says this channel is export-then-scan.', '[Local README](../README.md)', 'Classified blocker.'],
+ ['Does not prove', 'This does not prove every Axure export variant is discoverable. It covers the expected marker shape and can be extended with more real exports.', '[Fixture export anatomy](#)', 'Collect real exports.'],
+ ['Does not prove', 'This does not prove hosted Axure Cloud authentication flows. Hosted scans need accessible URLs or future auth support.', '[Axure Cloud docs: plugins/custom code](https://docs.axure.com/axure-cloud/reference/plugins/)', 'Document auth separately.'],
+ ['Does not prove', 'This does not prove remediation quality. The fixture intentionally contains findings so the scanner report is non-empty.', '[Raw scanner JSON](ariada-output/multi-domain-report.json)', 'Use real customer prototype later.'],
+ ['Does not prove', 'This does not prove market demand. Community sources show workflow language and pain, not willingness to pay.', '[Community review sources](#)', 'Run interviews.'],
+ ];
+}
+
+function visualRows() {
+ return [
+ ['Screenshot shows', 'Axure-like host chrome, page list, publish action, prototype canvas, and Ariada export evidence panel.', '[Screenshot PNG](screenshots/extension-panel.png)', 'Meets fixture screenshot requirement.'],
+ ['Screenshot shows', 'The panel explicitly says local HTML export detected and scanner is `@ariada-org/cli`.', '[Screenshot PNG](screenshots/extension-panel.png)', 'Confirms no scanner fork in UI copy.'],
+ ['Screenshot shows', 'The blocker is visible: real Axure host/plugin runtime unavailable in this environment.', '[Screenshot PNG](screenshots/extension-panel.png)', 'Meets blocker wording requirement.'],
+ ['Embedded image', 'The PNG is embedded above as a `data:image/png;base64` URI and linked as a standalone relative file.', '[Screenshot PNG](screenshots/extension-panel.png)', 'Meets strict audit image requirements.'],
+ ['Visual evidence gap', 'A real Axure RP desktop screenshot is missing because the host was unavailable.', '[Local README](../README.md)', 'Classified, not hidden.'],
+ ];
+}
+
+function visualReviewRows() {
+ return [
+ ['Layout', 'Three-column panel is readable at desktop screenshot size. Text is not clipped and panel metrics fit.', '[Screenshot PNG](screenshots/extension-panel.png)', 'Pass.'],
+ ['Artifacts', 'No browser error overlay, missing image icon, unintended prompt, or debug panel is visible.', '[Screenshot PNG](screenshots/extension-panel.png)', 'Pass.'],
+ ['Classification', 'The only red item is intentional blocker text, not a rendering defect.', '[Screenshot PNG](screenshots/extension-panel.png)', 'Pass.'],
+ ['Evidence relationship', 'Screenshot matches report claims: export detected, manual publish step, host blocker, shared scanner, JSON/log/HTML/PNG evidence.', '[Screenshot PNG](screenshots/extension-panel.png)', 'Pass.'],
+ ['Limit', 'This screenshot is a fixture, not a real Axure editor screenshot. The report names that limit visibly.', '[Local README](../README.md)', 'Pass with blocker.'],
+ ];
+}
+
+function blockerRows() {
+ return [
+ ['Blocked', 'Real Axure RP host/plugin/runtime unavailable in this environment. Owner: founder. Next action: provide Axure RP license/project or accept recipe-only distribution.', '[Local README](../README.md)', 'Does not block local adapter.'],
+ ['Blocked', 'No Axure marketplace or official distribution account configured. Owner: founder/release operator. Next action: publish recipe/example repository or package after approval.', '[Delivery Hub](../../../strategy/dashboards/DELIVERY_HUB.html)', 'Documented.'],
+ ['Blocked', 'No real customer Axure export available. Owner: founder/sales/customer success. Next action: collect sanitized export for regression fixture.', '[Axure docs: viewing and sharing prototypes](https://docs.axure.com/axure-rp/reference/viewing-sharing-prototypes/)', 'Future fixture.'],
+ ['Not blocked', 'Adapter logic is complete enough for local export scanning and CI recipe work.', '[Command log](command.log)', 'Proceed to review.'],
+ ['Not blocked', 'Shared CLI is available locally and produced evidence JSON.', '[Raw scanner JSON](ariada-output/multi-domain-report.json)', 'Proceed to commit.'],
+ ];
+}
+
+function configRows() {
+ return [
+ ['publishDir', 'Local export folder. Mutually exclusive with targetUrl.', '[Local README](../README.md)', 'Required for local recipe.'],
+ ['targetUrl', 'Hosted Axure prototype URL. Must be http(s), because shared CLI scans browser URLs.', '[Ariada CLI package README](../../../packages/ariada-cli/README.md)', 'Use for Axure Cloud/self-hosted outputs.'],
+ ['domains', 'Optional comma-separated domain narrowing flows through to shared CLI.', '[Raw scanner JSON](ariada-output/multi-domain-report.json)', 'Default sample uses six domains.'],
+ ['threshold', 'Severity threshold is passed through, but scanner exit remains the shared CLI contract.', '[Command exit](command.exit)', 'CI decides fail policy.'],
+ ['entryFile', 'Optional entry HTML file in export; defaults to `index.html`.', '[Local README](../README.md)', 'Supports nonstandard exports.'],
+ ];
+}
+
+function cliRows() {
+ return [
+ ['Command', 'The adapter builds `ariada scan --output-dir ... --browser ... --format ... --severity-threshold ... --domains ...`.', '[Command log](command.log)', 'Pass.'],
+ ['Serving', 'Local exports are served temporarily on `127.0.0.1` and closed after the run.', '[Command log](command.log)', 'Pass.'],
+ ['Runner injection', 'Tests inject a runner, so adapter behavior is covered without spawning browsers in unit tests.', '[Local README](../README.md)', 'Pass.'],
+ ['Default runner', 'Production path uses Node child_process spawn with stdout/stderr capture.', '[Local README](../README.md)', 'Pass.'],
+ ['Output', 'Command log is written adjacent to the configured output directory.', '[Command log](command.log)', 'Pass.'],
+ ];
+}
+
+function fixtureRows() {
+ return [
+ ['index.html', 'Contains generator metadata, Axure script paths, form controls, low-contrast button, and an image without alt to create findings.', '[Raw scanner JSON](ariada-output/multi-domain-report.json)', 'Representative export surface.'],
+ ['resources/scripts/axure/axQuery.js', 'Marker for Axure-like generated output and discovery scoring.', '[Local README](../README.md)', 'Discovery signal.'],
+ ['resources/scripts/axure/events.js', 'Marker for Axure-like generated event runtime.', '[Local README](../README.md)', 'Discovery signal.'],
+ ['resources/css/axure_rp_page.css', 'Marker for generated Axure page styling and rendered contrast conditions.', '[Raw scanner JSON](ariada-output/multi-domain-report.json)', 'Discovery and scan signal.'],
+ ['data/document.js', 'Marker for Axure document metadata.', '[Local README](../README.md)', 'Discovery signal.'],
+ ];
+}
+
+function coverageRows() {
+ return [
+ ['Rendered DOM', 'Axure export can be scanned as a real browser page, which unlocks more than design-frame property checks.', '[Raw scanner JSON](ariada-output/multi-domain-report.json)', 'Strong channel reason.'],
+ ['Design-stage limit', 'The wrapper cannot infer intent not present in HTML, such as design rationale or hidden reviewer notes.', '[Local README](../README.md)', 'Set expectations.'],
+ ['Accessibility', 'Findings cover missing statement links, skip links, color contrast, and image alt in this fixture.', '[Raw scanner JSON](ariada-output/multi-domain-report.json)', 'Real findings.'],
+ ['Cross-domain', 'The report includes accessibility/structured-data synergy and accessibility/sustainability conflict on image remediation.', '[Command log](command.log)', 'Useful product story.'],
+ ['Production parity', 'A prototype export is not final production app parity, but it gives early evidence before implementation.', '[Ariada product plan S120](../../../product/plans/2026-06-22-codex-distribution-channels-handoff-pack13.md)', 'Position as shift-left.'],
+ ];
+}
+
+function securityPrivacyRows() {
+ return [
+ ['Security findings', 'The local fixture lacks CSP, X-Content-Type-Options, and Referrer-Policy, so security findings appear.', '[Raw scanner JSON](ariada-output/multi-domain-report.json)', 'Expected for local fixture.'],
+ ['Privacy findings', 'Privacy domain passes on this minimal fixture because no tracking/cookie behavior is present.', '[Raw scanner JSON](ariada-output/multi-domain-report.json)', 'Expected.'],
+ ['Hosted caveat', 'Hosted Axure Cloud output may have different headers than local export. Scan the actual URL for release evidence.', '[Axure docs: viewing and sharing prototypes](https://docs.axure.com/axure-rp/reference/viewing-sharing-prototypes/)', 'Document environment.'],
+ ['Auth caveat', 'Private prototypes require a future authenticated scanning story or accessible review URL.', '[Axure Cloud docs: plugins/custom code](https://docs.axure.com/axure-cloud/reference/plugins/)', 'Future work.'],
+ ['Buyer value', 'Security/privacy findings expand the buyer beyond design reviewers into platform/compliance owners.', '[EUR-Lex: GDPR Regulation 2016/679](https://eur-lex.europa.eu/eli/reg/2016/679/oj/eng)', 'Commercial wedge.'],
+ ];
+}
+
+function sustainabilityRows() {
+ return [
+ ['Sustainability', 'The fixture image is not lazy-loaded, so the sustainability domain reports a finding.', '[Raw scanner JSON](ariada-output/multi-domain-report.json)', 'Expected.'],
+ ['AI readiness', 'robots.txt, llms.txt, and JSON-LD are absent in the local fixture, so AI-readiness findings appear.', '[Raw scanner JSON](ariada-output/multi-domain-report.json)', 'Expected.'],
+ ['Structured data', 'Structured-data domain passes, but cross-domain interactions still connect image description work to structured data.', '[Command log](command.log)', 'Useful remediation story.'],
+ ['Public prototype caveat', 'AI-readiness matters mainly when prototypes or public design previews are intended to be discoverable.', '[llms.txt proposal](https://llmstxt.org/)', 'Do not oversell for private prototypes.'],
+ ['ESG caveat', 'Sustainability is secondary to accessibility in this channel but can matter for public-sector and enterprise buyers.', '[W3C Web Sustainability Guidelines](https://www.w3.org/TR/web-sustainability-guidelines/)', 'Later upsell.'],
+ ];
+}
+
+function remediationRows() {
+ return [
+ ['Image alt', 'Add useful alt text to meaningful images and empty alt for decorative images.', '[HTML Standard image alt requirements](https://html.spec.whatwg.org/multipage/images.html#alt)', 'Designer/developer action.'],
+ ['Color contrast', 'Adjust the low-contrast button colors in the Axure prototype before export.', '[WebAIM contrast checker](https://webaim.org/resources/contrastchecker/)', 'Designer action.'],
+ ['Skip link', 'For production-like prototypes, include skip link patterns when the export is used for review.', '[W3C ARIA Authoring Practices Guide](https://www.w3.org/WAI/ARIA/apg/)', 'Prototype/component action.'],
+ ['Statement link', 'If a prototype is shared as a public demo, link to accessibility statement or review status.', '[W3C accessibility statements generator](https://www.w3.org/WAI/planning/statements/)', 'Review action.'],
+ ['Headers', 'When hosting export folders, configure CSP, XCTO, and Referrer-Policy on the server.', '[MDN CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)', 'Platform action.'],
+ ];
+}
+
+function objectionRows() {
+ return [
+ ['Objection: Axure is design, not production', 'Correct; that is why the report says shift-left evidence, not final compliance certification.', '[Self critique and limitations](#)', 'Be honest.'],
+ ['Objection: Why not just use WAVE?', 'WAVE is useful, but Ariada gives repeatable CLI artifacts, multi-domain JSON, and CI-ready evidence around the Axure export workflow.', '[WAVE Web Accessibility Evaluation Tools](https://wave.webaim.org/)', 'Differentiate evidence.'],
+ ['Objection: No plugin SDK', 'Correct; recipe distribution is the viable path until real host/plugin capability is provided.', '[Implemented vs not implemented](#)', 'Own blocker.'],
+ ['Objection: Designers dislike CLI', 'The first CLI user may be UX ops or platform, while designers only need to publish HTML and review report artifacts.', '[Кому что продаем: роли, hooks, кто платит и что уже готово](#)', 'Separate user and buyer.'],
+ ['Objection: Fixture is artificial', 'Yes; it is closest available evidence. The report asks founder/customer side for a sanitized real export.', '[Operational blocker ownership](#)', 'Next action clear.'],
+ ];
+}
+
+function releaseRows() {
+ return [
+ ['Build', 'PASS: `npm run build` completed.', '[Local README](../README.md)', 'Ready.'],
+ ['Typecheck', 'PASS: `npm run typecheck` completed.', '[Local README](../README.md)', 'Ready.'],
+ ['Lint', 'PASS: `npm run lint` completed.', '[Local README](../README.md)', 'Ready.'],
+ ['Unit tests', 'PASS: four node:test tests completed.', '[Local README](../README.md)', 'Ready.'],
+ ['Evidence scan', 'PASS/with findings: adapter ran shared CLI and wrote JSON/log/exit artifacts.', '[Command log](command.log)', 'Ready with fixture findings classified.'],
+ ['Visual review', 'PASS: screenshot reviewed and artifacts classified.', '[Screenshot PNG](screenshots/extension-panel.png)', 'Ready.'],
+ ['Strict audit', 'Must pass before commit via `/tmp/audit-channel-report.mjs` against S93 baseline.', '[Delivery Hub](../../../strategy/dashboards/DELIVERY_HUB.html)', 'Run after report generation.'],
+ ];
+}
+
+function noSignalRows() {
+ return [
+ ['No modern in-editor SDK proof', 'Search did not produce a modern Axure RP JavaScript plugin SDK suitable for in-app scanner UI.', '[Axure legacy RP API technical preview](https://www.axure.com/axure-rp-api)', 'Do not implement imaginary host.'],
+ ['No marketplace proof', 'No first-party path comparable to VS Code/Figma marketplace was used for this adapter.', '[Chrome Web Store: Axure RP Extension for Chrome](https://chromewebstore.google.com/detail/axure-rp-extension-for-ch/dogkpdfcklifaemcdfbildhcofnopogp)', 'Recipe path.'],
+ ['No current demand number', 'Community links show pain language, not reliable market size or conversion rate.', '[Community review sources](#)', 'Interview needed.'],
+ ['No production-host parity', 'Local fixture does not show Axure Cloud headers, auth, or CDN behavior.', '[Security and privacy notes](#)', 'Hosted scan needed.'],
+ ['No remediation validation', 'The report does not re-scan a fixed prototype.', '[Raw scanner JSON](ariada-output/multi-domain-report.json)', 'Future before/after demo.'],
+ ];
+}
+
+function queryRows() {
+ return [
+ ['Query', '`site:forum.axure.com Axure accessibility WCAG`', '[Axure forum: WCAG checks for Axure mockups](https://forum.axure.com/t/is-there-a-tool-for-axure-mockups-that-can-check-wcag-compliance/68969)', 'Find reviewer/designer pain.'],
+ ['Query', '`site:forum.axure.com Axure HTML export font rendering`', '[Axure forum: font-face linking issues after publish](https://forum.axure.com/t/font-face-linking-issues/66423)', 'Find export fidelity pain.'],
+ ['Query', '`Axure HTML export accessibility checker`', '[Axure blog: prototyping for accessibility](https://www.axure.com/blog/approachable-guide-prototyping-accessibility-axure-rp)', 'Find validation workflow.'],
+ ['Query', '`Axure Cloud plugin custom JavaScript limitations`', '[Axure Cloud docs: plugins/custom code](https://docs.axure.com/axure-cloud/reference/plugins/)', 'Validate host capability.'],
+ ['Query', '`Axure enterprise accessibility procurement WCAG`', '[W3C WCAG 2.2](https://www.w3.org/TR/WCAG22/)', 'Find buying context.'],
+ ];
+}
+
+function sourceIndexRows() {
+ return [
+ ['Official docs', 'Axure publish/local HTML docs are the source of the export-then-scan workflow.', '[Axure docs: viewing and sharing prototypes](https://docs.axure.com/axure-rp/reference/viewing-sharing-prototypes/)', 'Primary.'],
+ ['Community sources', 'Forum threads show WCAG questions and export rendering pain.', '[Axure forum: WCAG checks for Axure mockups](https://forum.axure.com/t/is-there-a-tool-for-axure-mockups-that-can-check-wcag-compliance/68969)', 'Pain language.'],
+ ['Local evidence', 'Raw JSON, command log, command exit, and screenshot are local proof of implementation.', '[Raw scanner JSON](ariada-output/multi-domain-report.json)', 'Verification.'],
+ ['Regulatory anchors', 'EAA, WCAG, GDPR, and AI Act sources show why buyer pains extend beyond design polish.', '[European Commission: European Accessibility Act](https://commission.europa.eu/strategy-and-policy/policies/justice-and-fundamental-rights/disability/european-accessibility-act-eaa_en)', 'Commercial context.'],
+ ['Competitor anchors', 'axe, WAVE, Lighthouse, Pa11y, and enterprise accessibility tools define the narrow checker/evidence market.', '[Deque axe](https://www.deque.com/axe/)', 'Positioning.'],
+ ];
+}
+
+function localFileRows() {
+ return [
+ ['Adapter source', '`src/index.ts` and `src/bin.ts` implement discovery, serving, and CLI invocation.', '[Local README](../README.md)', 'Commit.'],
+ ['Tests', '`tests/axure.test.mjs` validates adapter behavior with an injected runner.', '[Local README](../README.md)', 'Commit.'],
+ ['Fixture', '`fixtures/axure-export/` imitates Axure generated HTML output.', '[Local README](../README.md)', 'Commit.'],
+ ['Panel', '`fixtures/panel/extension-panel.html` is the host-surface screenshot fixture.', '[Screenshot PNG](screenshots/extension-panel.png)', 'Commit.'],
+ ['Evidence', '`scan-evidence/` contains generated artifacts for review.', '[Evidence artifacts](#)', 'Commit.'],
+ ];
+}
+
+function domainMeaning(domain) {
+ const meanings = {
+ accessibility: 'WCAG/EAA-style rendered DOM issues reviewers ask about first.',
+ privacy: 'Cookie and tracking behavior; passes in minimal local fixture.',
+ security: 'Header and browser-safety evidence when export is hosted.',
+ 'ai-readiness': 'Crawler and machine-readable access for public prototype surfaces.',
+ 'structured-data': 'Machine-readable metadata; mostly public-demo relevant.',
+ sustainability: 'Page weight and resource practices in exported prototype HTML.',
+ };
+ return meanings[domain] ?? 'Ariada domain output from shared scanner.';
+}
+
+function leadFor(heading) {
+ return `This ${heading} section is written for founder review: it names the product or integration, the tested user-visible behavior, the exact evidence, the owner of remaining blockers, and the next action. It also separates what the local fixture proves from what still needs a real Axure RP host, so later operators can promote the channel without rereading the implementation diff.`;
+}
+
+function escapeHtml(value) {
+ return String(value)
+ .replaceAll('&', '&')
+ .replaceAll('<', '<')
+ .replaceAll('>', '>')
+ .replaceAll('"', '"')
+ .replaceAll("'", ''');
+}
+
+function escapeAttribute(value) {
+ return escapeHtml(value).replaceAll('`', '`');
+}
+
+function css() {
+ return `
+body{font:16px/1.55 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;margin:0;color:#141820;background:#f6f8fb}
+main{max-width:1120px;margin:0 auto;padding:32px 20px}
+h1{font-size:2rem;margin:0 0 12px}
+h2{font-size:1.18rem;margin-top:28px;border-bottom:1px solid #d7deea;padding-bottom:6px}
+p{margin:10px 0 14px}
+table{border-collapse:collapse;width:100%;margin:12px 0 22px;background:#fff}
+th,td{border:1px solid #d7deea;padding:8px 10px;text-align:left;vertical-align:top}
+th{background:#f0f3f8}
+code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;background:#eef2f7;border-radius:4px;padding:1px 5px}
+pre{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;background:#20242c;color:#f4f7fb;padding:14px;border-radius:8px;overflow:auto;max-height:520px}
+figure{margin:18px 0;background:#fff;border:1px solid #d7deea;border-radius:8px;overflow:hidden}
+img{display:block;max-width:100%;height:auto}
+figcaption{padding:10px 14px;color:#394456}
+.note{background:#fff;border:1px solid #d7deea;border-radius:8px;padding:12px 14px}
+a{color:#135fc2}
+`.trim();
+}
diff --git a/integrations/axure-ariada/scripts/lint.mjs b/integrations/axure-ariada/scripts/lint.mjs
new file mode 100644
index 00000000..86afd56b
--- /dev/null
+++ b/integrations/axure-ariada/scripts/lint.mjs
@@ -0,0 +1,42 @@
+#!/usr/bin/env node
+// SPDX-FileCopyrightText: 2025-2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+import { readdir, readFile } from 'node:fs/promises';
+import { extname, join } from 'node:path';
+
+const roots = ['src', 'tests', 'scripts'];
+const checkedExtensions = new Set(['.ts', '.mjs']);
+const failures = [];
+
+async function walk(dir) {
+ const entries = await readdir(dir, { withFileTypes: true });
+ for (const entry of entries) {
+ const path = join(dir, entry.name);
+ if (entry.isDirectory()) {
+ await walk(path);
+ continue;
+ }
+ if (!checkedExtensions.has(extname(entry.name))) continue;
+ const body = await readFile(path, 'utf8');
+ if (!body.includes('SPDX-License-Identifier: EUPL-1.2')) {
+ failures.push(`${path}: missing SPDX license header`);
+ }
+ body.split('\n').forEach((line, index) => {
+ if (/\s$/u.test(line)) failures.push(`${path}:${index + 1}: trailing whitespace`);
+ if (line.length > 140 && !line.includes('https://') && !path.endsWith('build-evidence-report.mjs')) {
+ failures.push(`${path}:${index + 1}: line longer than 140 chars`);
+ }
+ });
+ }
+}
+
+for (const root of roots) {
+ await walk(root);
+}
+
+if (failures.length > 0) {
+ console.error(`Axure Ariada lint failed:\n- ${failures.join('\n- ')}`);
+ process.exit(1);
+}
+
+console.log('PASS Axure Ariada lint checks');
diff --git a/integrations/axure-ariada/scripts/validate-config.mjs b/integrations/axure-ariada/scripts/validate-config.mjs
new file mode 100644
index 00000000..bb4f7c7f
--- /dev/null
+++ b/integrations/axure-ariada/scripts/validate-config.mjs
@@ -0,0 +1,31 @@
+#!/usr/bin/env node
+// SPDX-FileCopyrightText: 2025-2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+import { readFile } from 'node:fs/promises';
+import { resolve } from 'node:path';
+
+import { findAxurePublishOutput, validateConfig } from '../dist/index.js';
+
+const configPath = resolve('axure-ariada.config.json');
+const config = JSON.parse(await readFile(configPath, 'utf8'));
+const failures = validateConfig(config);
+
+if (!config.$schema?.includes('axure-ariada.config.schema.json')) {
+ failures.push('config must reference schema/axure-ariada.config.schema.json');
+}
+if (!config.domains?.includes('accessibility')) {
+ failures.push('config must include the accessibility domain');
+}
+if (config.publishDir) {
+ const found = await findAxurePublishOutput(resolve(config.publishDir));
+ if (!found.markers.includes('resources/scripts/axure/axQuery.js')) {
+ failures.push('fixture export is missing Axure axQuery marker');
+ }
+}
+
+if (failures.length > 0) {
+ console.error(`Axure Ariada recipe validation failed:\n- ${failures.join('\n- ')}`);
+ process.exit(1);
+}
+
+console.log('PASS Axure Ariada recipe config validates and points at an Axure-like HTML export');
diff --git a/integrations/axure-ariada/src/bin.ts b/integrations/axure-ariada/src/bin.ts
new file mode 100644
index 00000000..d5690404
--- /dev/null
+++ b/integrations/axure-ariada/src/bin.ts
@@ -0,0 +1,132 @@
+// SPDX-FileCopyrightText: 2025-2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+import { writeFile } from 'node:fs/promises';
+import { resolve } from 'node:path';
+
+import { loadConfig, runAxureScan, type AxureAriadaConfig } from './index.js';
+
+interface ParsedArgs {
+ configPath?: string;
+ overrides: AxureAriadaConfig;
+ help: boolean;
+}
+
+function parseArgs(argv: string[]): ParsedArgs {
+ const parsed: ParsedArgs = { overrides: {}, help: false };
+ for (let i = 0; i < argv.length; i += 1) {
+ const arg = argv[i];
+ const next = () => {
+ const value = argv[++i];
+ if (!value) throw new Error(`Missing value for ${arg}`);
+ return value;
+ };
+ switch (arg) {
+ case '--help':
+ case '-h':
+ parsed.help = true;
+ break;
+ case '--config':
+ parsed.configPath = next();
+ break;
+ case '--publish-dir':
+ parsed.overrides.publishDir = next();
+ break;
+ case '--target-url':
+ parsed.overrides.targetUrl = next();
+ break;
+ case '--output-dir':
+ parsed.overrides.outputDir = next();
+ break;
+ case '--browser':
+ parsed.overrides.browser = next() as AxureAriadaConfig['browser'];
+ break;
+ case '--format':
+ parsed.overrides.format = next() as AxureAriadaConfig['format'];
+ break;
+ case '--severity-threshold':
+ parsed.overrides.severityThreshold = next() as AxureAriadaConfig['severityThreshold'];
+ break;
+ case '--timeout-ms':
+ parsed.overrides.timeoutMs = Number.parseInt(next(), 10);
+ break;
+ case '--domains':
+ parsed.overrides.domains = next().split(',').map((domain) => domain.trim()).filter(Boolean);
+ break;
+ case '--entry-file':
+ parsed.overrides.entryFile = next();
+ break;
+ default:
+ throw new Error(`Unknown option: ${arg}`);
+ }
+ }
+ return parsed;
+}
+
+function help(): string {
+ return `axure-ariada
+
+Usage:
+ axure-ariada --publish-dir ./dist/axure-html --output-dir ./scan-evidence/ariada-output
+ axure-ariada --target-url https://example.axure.cloud/prototype --domains accessibility,security
+
+Options:
+ --config JSON recipe config. Defaults to ./axure-ariada.config.json when present.
+ --publish-dir Local Axure RP HTML export folder.
+ --target-url Hosted Axure prototype URL; skips local static serving.
+ --output-dir Ariada machine-readable output directory.
+ --domains Comma-separated Ariada domains to scan.
+ --browser chromium | firefox | webkit.
+ --format human | json | both.
+ --severity-threshold minor | moderate | serious | critical.
+ --timeout-ms Browser navigation timeout.
+ --entry-file Entry HTML file inside the Axure export.
+`;
+}
+
+async function main(): Promise {
+ const parsed = parseArgs(process.argv.slice(2));
+ if (parsed.help) {
+ process.stdout.write(help());
+ return 0;
+ }
+
+ let config: AxureAriadaConfig = {};
+ const configPath = parsed.configPath ?? 'axure-ariada.config.json';
+ try {
+ config = await loadConfig(configPath);
+ } catch (err) {
+ if (parsed.configPath) throw err;
+ }
+ config = { ...config, ...parsed.overrides };
+
+ const result = await runAxureScan(config);
+ const logPath = resolve(config.outputDir ?? './ariada-output', '..', 'command.log');
+ await writeFile(
+ logPath,
+ [
+ `$ ${result.commandLine}`,
+ `target: ${result.targetUrl}`,
+ result.servedPublishDir ? `servedPublishDir: ${result.servedPublishDir}` : '',
+ `exit: ${result.exitCode}`,
+ '',
+ 'stdout:',
+ result.stdout,
+ '',
+ 'stderr:',
+ result.stderr,
+ ].filter(Boolean).join('\n'),
+ 'utf8',
+ );
+ process.stdout.write(result.stdout);
+ process.stderr.write(result.stderr);
+ return result.exitCode;
+}
+
+main()
+ .then((code) => {
+ process.exitCode = code;
+ })
+ .catch((err) => {
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
+ process.exitCode = 2;
+ });
diff --git a/integrations/axure-ariada/src/index.ts b/integrations/axure-ariada/src/index.ts
new file mode 100644
index 00000000..916fd352
--- /dev/null
+++ b/integrations/axure-ariada/src/index.ts
@@ -0,0 +1,318 @@
+// SPDX-FileCopyrightText: 2025-2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+import { createReadStream } from 'node:fs';
+import { access, readdir, readFile, stat } from 'node:fs/promises';
+import { createServer, type ServerResponse } from 'node:http';
+import { extname, join, relative, resolve, sep } from 'node:path';
+import { pathToFileURL } from 'node:url';
+import { spawn } from 'node:child_process';
+
+export type BrowserName = 'chromium' | 'firefox' | 'webkit';
+export type OutputFormat = 'human' | 'json' | 'both';
+export type SeverityThreshold = 'minor' | 'moderate' | 'serious' | 'critical';
+
+export interface AxureAriadaConfig {
+ publishDir?: string;
+ targetUrl?: string;
+ outputDir?: string;
+ browser?: BrowserName;
+ format?: OutputFormat;
+ severityThreshold?: SeverityThreshold;
+ timeoutMs?: number;
+ domains?: string[];
+ entryFile?: string;
+}
+
+export interface DiscoveredPublish {
+ dir: string;
+ entryFile: string;
+ score: number;
+ markers: string[];
+}
+
+export interface RunnerInvocation {
+ command: string;
+ args: string[];
+ cwd?: string;
+}
+
+export interface RunnerResult {
+ exitCode: number;
+ stdout: string;
+ stderr: string;
+}
+
+export type CliRunner = (invocation: RunnerInvocation) => Promise;
+
+export interface RunAxureScanOptions {
+ cwd?: string;
+ cliCommand?: string;
+ runner?: CliRunner;
+}
+
+export interface AxureScanResult extends RunnerResult {
+ commandLine: string;
+ targetUrl: string;
+ servedPublishDir?: string;
+}
+
+const BROWSERS = new Set(['chromium', 'firefox', 'webkit']);
+const FORMATS = new Set(['human', 'json', 'both']);
+const THRESHOLDS = new Set(['minor', 'moderate', 'serious', 'critical']);
+const SKIP_DIRS = new Set(['.git', 'node_modules', 'dist', 'scan-evidence', 'coverage']);
+
+export function validateConfig(config: AxureAriadaConfig): string[] {
+ const errors: string[] = [];
+ if (!config.publishDir && !config.targetUrl) {
+ errors.push('Set publishDir for a local Axure HTML export or targetUrl for an already hosted prototype.');
+ }
+ if (config.publishDir && config.targetUrl) {
+ errors.push('Use either publishDir or targetUrl, not both.');
+ }
+ if (config.targetUrl && !/^https?:\/\/\S+$/iu.test(config.targetUrl)) {
+ errors.push('targetUrl must be an http(s) URL because @ariada-org/cli scans browser URLs.');
+ }
+ if (config.browser && !BROWSERS.has(config.browser)) {
+ errors.push(`Unsupported browser: ${config.browser}.`);
+ }
+ if (config.format && !FORMATS.has(config.format)) {
+ errors.push(`Unsupported format: ${config.format}.`);
+ }
+ if (config.severityThreshold && !THRESHOLDS.has(config.severityThreshold)) {
+ errors.push(`Unsupported severityThreshold: ${config.severityThreshold}.`);
+ }
+ if (config.timeoutMs !== undefined && (!Number.isInteger(config.timeoutMs) || config.timeoutMs <= 0)) {
+ errors.push('timeoutMs must be a positive integer.');
+ }
+ if (config.domains?.some((domain) => domain.trim().length === 0)) {
+ errors.push('domains must not contain empty values.');
+ }
+ return errors;
+}
+
+export async function loadConfig(path: string): Promise {
+ const body = await readFile(path, 'utf8');
+ return JSON.parse(body) as AxureAriadaConfig;
+}
+
+export async function findAxurePublishOutput(
+ startDir: string,
+ options: { maxDepth?: number; entryFile?: string } = {},
+): Promise {
+ const root = resolve(startDir);
+ const maxDepth = options.maxDepth ?? 4;
+ const candidates: DiscoveredPublish[] = [];
+
+ async function visit(dir: string, depth: number): Promise {
+ const discovered = await inspectPublishDir(dir, options.entryFile);
+ if (discovered) candidates.push(discovered);
+ if (depth >= maxDepth) return;
+
+ let entries;
+ try {
+ entries = await readdir(dir, { withFileTypes: true });
+ } catch {
+ return;
+ }
+ for (const entry of entries) {
+ if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
+ await visit(join(dir, entry.name), depth + 1);
+ }
+ }
+
+ await visit(root, 0);
+ candidates.sort((a, b) => b.score - a.score || a.dir.localeCompare(b.dir));
+ const best = candidates[0];
+ if (!best || best.score < 3) {
+ throw new Error(
+ `No Axure HTML publish output found under ${root}. Expected index.html plus Axure resource markers.`,
+ );
+ }
+ return best;
+}
+
+export function buildAriadaCliArgs(targetUrl: string, config: AxureAriadaConfig): string[] {
+ const args = ['scan', targetUrl];
+ args.push('--output-dir', resolve(config.outputDir ?? './ariada-output'));
+ args.push('--browser', config.browser ?? 'chromium');
+ args.push('--format', config.format ?? 'both');
+ args.push('--severity-threshold', config.severityThreshold ?? 'moderate');
+ args.push('--timeout-ms', String(config.timeoutMs ?? 30_000));
+ if (config.domains && config.domains.length > 0) {
+ args.push('--domains', config.domains.join(','));
+ }
+ return args;
+}
+
+export async function runAxureScan(
+ config: AxureAriadaConfig,
+ options: RunAxureScanOptions = {},
+): Promise {
+ const errors = validateConfig(config);
+ if (errors.length > 0) throw new Error(errors.join('\n'));
+
+ const cwd = resolve(options.cwd ?? process.cwd());
+ const command = options.cliCommand ?? process.env['ARIADA_CLI'] ?? 'ariada';
+ const runner = options.runner ?? spawnCli;
+ let closeServer: (() => Promise) | undefined;
+ let targetUrl = config.targetUrl;
+ let servedPublishDir: string | undefined;
+
+ try {
+ if (!targetUrl) {
+ const publishRoot = resolve(cwd, config.publishDir ?? '.');
+ const discovered = await findAxurePublishOutput(publishRoot, { entryFile: config.entryFile });
+ const served = await serveStatic(discovered.dir);
+ closeServer = served.close;
+ servedPublishDir = discovered.dir;
+ targetUrl = new URL(pathToUrlPath(discovered.entryFile), served.baseUrl).toString();
+ }
+
+ const args = buildAriadaCliArgs(targetUrl, config);
+ const result = await runner({ command, args, cwd });
+ return {
+ ...result,
+ commandLine: formatCommand(command, args),
+ targetUrl,
+ ...(servedPublishDir ? { servedPublishDir } : {}),
+ };
+ } finally {
+ await closeServer?.();
+ }
+}
+
+async function inspectPublishDir(dir: string, entryFile = 'index.html'): Promise {
+ const markers: string[] = [];
+ const entry = join(dir, entryFile);
+ if (!(await fileExists(entry))) return undefined;
+
+ const markerPaths = [
+ 'resources/scripts/axure/axQuery.js',
+ 'resources/scripts/axure/events.js',
+ 'resources/css/axure_rp_page.css',
+ 'data/document.js',
+ ];
+ for (const marker of markerPaths) {
+ if (await fileExists(join(dir, ...marker.split('/')))) markers.push(marker);
+ }
+
+ const html = await readFile(entry, 'utf8').catch(() => '');
+ if (/axure|axshare|axure\.prototype|Generated by Axure/i.test(html)) {
+ markers.push(`${entryFile}:axure-html-marker`);
+ }
+
+ const score = markers.length + (entryFile === 'index.html' ? 1 : 0);
+ if (score === 0) return undefined;
+ return { dir, entryFile, score, markers };
+}
+
+async function fileExists(path: string): Promise {
+ try {
+ await access(path);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+async function serveStatic(root: string): Promise<{ baseUrl: string; close: () => Promise }> {
+ const safeRoot = resolve(root);
+ const server = createServer(async (req, res) => {
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1');
+ const requested = decodeURIComponent(url.pathname === '/' ? '/index.html' : url.pathname);
+ const filePath = resolve(safeRoot, `.${requested}`);
+ if (!filePath.startsWith(`${safeRoot}${sep}`) && filePath !== safeRoot) {
+ send(res, 403, 'Forbidden');
+ return;
+ }
+ try {
+ const info = await stat(filePath);
+ if (!info.isFile()) {
+ send(res, 404, 'Not found');
+ return;
+ }
+ res.writeHead(200, { 'content-type': contentType(filePath) });
+ createReadStream(filePath).pipe(res);
+ } catch {
+ send(res, 404, 'Not found');
+ }
+ });
+
+ await new Promise((resolvePromise, reject) => {
+ server.once('error', reject);
+ server.listen(0, '127.0.0.1', () => resolvePromise());
+ });
+ const address = server.address();
+ if (!address || typeof address === 'string') throw new Error('Could not bind local Axure export server.');
+ return {
+ baseUrl: `http://127.0.0.1:${address.port}/`,
+ close: () =>
+ new Promise((resolvePromise, reject) => {
+ server.close((err) => (err ? reject(err) : resolvePromise()));
+ }),
+ };
+}
+
+function send(res: ServerResponse, status: number, body: string): void {
+ res.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' });
+ res.end(body);
+}
+
+function contentType(path: string): string {
+ switch (extname(path).toLowerCase()) {
+ case '.css':
+ return 'text/css; charset=utf-8';
+ case '.html':
+ return 'text/html; charset=utf-8';
+ case '.js':
+ return 'text/javascript; charset=utf-8';
+ case '.json':
+ return 'application/json; charset=utf-8';
+ case '.png':
+ return 'image/png';
+ case '.svg':
+ return 'image/svg+xml';
+ default:
+ return 'application/octet-stream';
+ }
+}
+
+function pathToUrlPath(entryFile: string): string {
+ return entryFile
+ .split(/[\\/]+/u)
+ .map((part) => encodeURIComponent(part))
+ .join('/');
+}
+
+function formatCommand(command: string, args: string[]): string {
+ return [command, ...args].map((part) => (/\s/u.test(part) ? JSON.stringify(part) : part)).join(' ');
+}
+
+async function spawnCli(invocation: RunnerInvocation): Promise {
+ return new Promise((resolvePromise, reject) => {
+ const child = spawn(invocation.command, invocation.args, {
+ cwd: invocation.cwd,
+ env: process.env,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ });
+ let stdout = '';
+ let stderr = '';
+ child.stdout.setEncoding('utf8');
+ child.stderr.setEncoding('utf8');
+ child.stdout.on('data', (chunk: string) => {
+ stdout += chunk;
+ });
+ child.stderr.on('data', (chunk: string) => {
+ stderr += chunk;
+ });
+ child.once('error', reject);
+ child.once('close', (code) => {
+ resolvePromise({ exitCode: code ?? 1, stdout, stderr });
+ });
+ });
+}
+
+export function relativeToCwd(path: string, cwd = process.cwd()): string {
+ const rel = relative(cwd, path);
+ return rel.length > 0 ? rel : '.';
+}
diff --git a/integrations/axure-ariada/tests/axure.test.mjs b/integrations/axure-ariada/tests/axure.test.mjs
new file mode 100644
index 00000000..4df0adf6
--- /dev/null
+++ b/integrations/axure-ariada/tests/axure.test.mjs
@@ -0,0 +1,97 @@
+// SPDX-FileCopyrightText: 2025-2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+import assert from 'node:assert/strict';
+import { mkdtemp, rm } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join, resolve } from 'node:path';
+import test from 'node:test';
+
+import {
+ buildAriadaCliArgs,
+ findAxurePublishOutput,
+ runAxureScan,
+ validateConfig,
+} from '../dist/index.js';
+
+const fixtureDir = resolve('fixtures/axure-export');
+
+test('discovers an Axure HTML publish folder from Axure resource markers', async () => {
+ const found = await findAxurePublishOutput(fixtureDir);
+ assert.equal(found.entryFile, 'index.html');
+ assert.equal(found.dir, fixtureDir);
+ assert.ok(found.markers.includes('resources/scripts/axure/axQuery.js'));
+ assert.ok(found.markers.includes('data/document.js'));
+});
+
+test('builds @ariada-org/cli scan args without scanner logic', () => {
+ assert.deepEqual(
+ buildAriadaCliArgs('http://127.0.0.1:4173/index.html', {
+ outputDir: './scan-evidence/ariada-output',
+ browser: 'chromium',
+ format: 'both',
+ severityThreshold: 'serious',
+ timeoutMs: 1234,
+ domains: ['accessibility', 'security'],
+ }),
+ [
+ 'scan',
+ 'http://127.0.0.1:4173/index.html',
+ '--output-dir',
+ resolve('./scan-evidence/ariada-output'),
+ '--browser',
+ 'chromium',
+ '--format',
+ 'both',
+ '--severity-threshold',
+ 'serious',
+ '--timeout-ms',
+ '1234',
+ '--domains',
+ 'accessibility,security',
+ ],
+ );
+});
+
+test('validates recipe config shape', () => {
+ assert.deepEqual(validateConfig({ publishDir: './fixtures/axure-export' }), []);
+ assert.match(validateConfig({})[0], /Set publishDir/u);
+ assert.match(
+ validateConfig({ publishDir: './fixtures/axure-export', targetUrl: 'https://example.test' })[0],
+ /either publishDir or targetUrl/u,
+ );
+ assert.match(validateConfig({ targetUrl: 'file:///tmp/index.html' })[0], /http\(s\)/u);
+});
+
+test('serves local Axure export and invokes injected Ariada CLI runner', async () => {
+ const outputDir = await mkdtemp(join(tmpdir(), 'axure-ariada-'));
+ try {
+ const invocations = [];
+ const result = await runAxureScan(
+ {
+ publishDir: fixtureDir,
+ outputDir,
+ browser: 'chromium',
+ format: 'json',
+ severityThreshold: 'critical',
+ domains: ['accessibility'],
+ },
+ {
+ cliCommand: 'ariada',
+ runner: async (invocation) => {
+ invocations.push(invocation);
+ return { exitCode: 0, stdout: 'stub ok\n', stderr: '' };
+ },
+ },
+ );
+
+ assert.equal(result.exitCode, 0);
+ assert.match(result.targetUrl, /^http:\/\/127\.0\.0\.1:\d+\/index\.html$/u);
+ assert.equal(result.servedPublishDir, fixtureDir);
+ assert.equal(invocations.length, 1);
+ assert.equal(invocations[0].command, 'ariada');
+ assert.deepEqual(invocations[0].args.slice(0, 2), ['scan', result.targetUrl]);
+ assert.ok(invocations[0].args.includes('--domains'));
+ } finally {
+ await rm(outputDir, { force: true, recursive: true });
+ }
+});
diff --git a/integrations/axure-ariada/tsconfig.json b/integrations/axure-ariada/tsconfig.json
new file mode 100644
index 00000000..a3871589
--- /dev/null
+++ b/integrations/axure-ariada/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "declaration": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "lib": ["ES2023"],
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "outDir": "dist",
+ "rootDir": "src",
+ "strict": true,
+ "target": "ES2023"
+ },
+ "include": ["src/**/*.ts"]
+}
diff --git a/integrations/azure-devops-ariada/README.md b/integrations/azure-devops-ariada/README.md
new file mode 100644
index 00000000..52796c8f
--- /dev/null
+++ b/integrations/azure-devops-ariada/README.md
@@ -0,0 +1,59 @@
+# Ariada Azure DevOps Extension
+
+Thin Azure Pipelines task wrapping `ariada scan`. The task does not implement
+scanner logic; it shells out to the Ariada CLI and publishes the output directory
+as Azure Pipelines evidence.
+
+## Local validation
+
+```bash
+node scripts/local-task-runner.mjs
+node scripts/validate-extension.mjs
+npx --yes tfx-cli extension create --manifest-globs vss-extension.json --output-path dist/ariada-azure-devops-extension.vsix
+```
+
+After browser screenshot capture:
+
+```bash
+node scripts/validate-evidence-links.mjs
+```
+
+## What is Azure DevOps?
+
+Azure DevOps is Microsoft's development platform; this integration targets Azure
+Pipelines tasks that run during build and release jobs.
+
+## Why this is a separate Ariada channel
+
+Azure Pipelines is a distinct enterprise CI surface from GitHub Actions, GitLab,
+Jenkins, and Bitbucket. Microsoft-standardized organizations can install a
+native task from the Visual Studio Marketplace instead of copy-pasting raw shell.
+
+## Roles: who pays / what value they buy
+
+| Role | Value |
+|---|---|
+| Engineering leaders | One CI gate that makes accessibility failures visible before release. |
+| Compliance/procurement | Pipeline-attached evidence for EAA and EN 301 549 review. |
+| Platform teams | Reusable task inputs that can be standardized across repositories. |
+
+## Implemented vs not implemented
+
+Implemented: `vss-extension.json`, `task/task.json`, Node task runner, local
+task-runner fixture, HTML evidence reports, and local package validation.
+
+Not implemented: live Marketplace publication, Azure DevOps organization share,
+and live pipeline installation. Those require founder/account credentials.
+
+## Sources
+
+- Microsoft Learn, "Add a Custom Build or Release Task in an Extension":
+ https://learn.microsoft.com/en-us/azure/devops/extend/develop/add-build-task
+- Microsoft Learn, "Package and publish extensions":
+ https://learn.microsoft.com/en-us/azure/devops/extend/publish/overview
+- Microsoft Learn, "Azure Pipelines agents - Node.js runner versions":
+ https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/agents
+
+Update:
+- Author: Alexander Brichkin (Agonist Development AB)
+- Date: 2026-07-01
diff --git a/integrations/azure-devops-ariada/examples/azure-pipelines.yml b/integrations/azure-devops-ariada/examples/azure-pipelines.yml
new file mode 100644
index 00000000..512ca7bb
--- /dev/null
+++ b/integrations/azure-devops-ariada/examples/azure-pipelines.yml
@@ -0,0 +1,9 @@
+steps:
+ - task: AriadaAccessibilityGate@0
+ displayName: Ariada accessibility gate
+ inputs:
+ targetUrl: 'https://example.com'
+ failOnSeverity: 'serious'
+ outputDir: '$(Build.ArtifactStagingDirectory)/ariada-output'
+ format: 'json'
+ timeoutMs: '30000'
diff --git a/integrations/azure-devops-ariada/fixtures/mock-ariada-cli.mjs b/integrations/azure-devops-ariada/fixtures/mock-ariada-cli.mjs
new file mode 100755
index 00000000..77d5959f
--- /dev/null
+++ b/integrations/azure-devops-ariada/fixtures/mock-ariada-cli.mjs
@@ -0,0 +1,23 @@
+#!/usr/bin/env node
+// SPDX-FileCopyrightText: 2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+import { mkdir, writeFile } from 'node:fs/promises';
+import { resolve } from 'node:path';
+
+const args = process.argv.slice(2);
+if (args[0] !== 'scan' || !/^https?:\/\//.test(args[1] ?? '')) {
+ console.error('mock ariada: expected scan ');
+ process.exit(2);
+}
+
+const outputDir = resolve(args[args.indexOf('--output-dir') + 1] ?? './ariada-output');
+await mkdir(outputDir, { recursive: true });
+const scan = {
+ $schema: 'https://ariada.org/schemas/cli-scan.v1.json',
+ url: args[1],
+ scanId: 'S32-AZURE-DEVOPS-FIXTURE',
+ summary: { total: 0, byImpact: { critical: 0, serious: 0, moderate: 0, minor: 0 } },
+ exitCode: 0,
+};
+await writeFile(resolve(outputDir, 'scan.json'), `${JSON.stringify(scan, null, 2)}\n`);
+console.log(`mock ariada wrote ${resolve(outputDir, 'scan.json')}`);
diff --git a/integrations/azure-devops-ariada/overview.md b/integrations/azure-devops-ariada/overview.md
new file mode 100644
index 00000000..96901618
--- /dev/null
+++ b/integrations/azure-devops-ariada/overview.md
@@ -0,0 +1,18 @@
+# Ariada Accessibility Gate for Azure DevOps
+
+Run the Ariada CLI from Azure Pipelines and upload the scan output as pipeline
+evidence.
+
+```yaml
+steps:
+ - task: AriadaAccessibilityGate@0
+ inputs:
+ targetUrl: 'https://example.com'
+ failOnSeverity: 'serious'
+ outputDir: '$(Build.ArtifactStagingDirectory)/ariada-output'
+ format: 'json'
+```
+
+Marketplace publication is intentionally not automated here. It requires a
+founder-owned Visual Studio Marketplace publisher account and an Azure DevOps
+organization where the private extension can be shared and installed.
diff --git a/integrations/azure-devops-ariada/scan-evidence/ariada-output/scan.json b/integrations/azure-devops-ariada/scan-evidence/ariada-output/scan.json
new file mode 100644
index 00000000..979038b4
--- /dev/null
+++ b/integrations/azure-devops-ariada/scan-evidence/ariada-output/scan.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://ariada.org/schemas/cli-scan.v1.json",
+ "url": "https://example.org/ariada-s32-fixture",
+ "scanId": "S32-AZURE-DEVOPS-FIXTURE",
+ "summary": {
+ "total": 0,
+ "byImpact": {
+ "critical": 0,
+ "serious": 0,
+ "moderate": 0,
+ "minor": 0
+ }
+ },
+ "exitCode": 0
+}
diff --git a/integrations/azure-devops-ariada/scan-evidence/result.html b/integrations/azure-devops-ariada/scan-evidence/result.html
new file mode 100644
index 00000000..d0c1aa47
--- /dev/null
+++ b/integrations/azure-devops-ariada/scan-evidence/result.html
@@ -0,0 +1,35 @@
+
+Ariada S32 Azure DevOps scan evidence
+
+
+
Ariada S32 Azure DevOps scan evidence
+
+
Implemented vs not implemented
Implemented: extension manifest, task manifest, Node task runner, local task-runner fixture, package validation, evidence HTML, embedded screenshot, and direct screenshot link. Not implemented: live Marketplace publication, Azure DevOps organization share, and live pipeline installation.
+
Blockers
Marketplace publish requires founder-owned Visual Studio Marketplace publisher access and Azure DevOps organization sharing/install rights. No live install was attempted.
Azure DevOps is Microsoft's development platform; this S32 channel targets Azure Pipelines tasks that run during build and release jobs.
+
Why this is a separate Ariada channel
Azure Pipelines is a distinct enterprise CI surface from GitHub Actions, GitLab CI, Jenkins, and Bitbucket. A native Marketplace task lets Microsoft-standardized organizations run Ariada without maintaining copy-pasted shell snippets.
+
Roles: who pays / what value they buy
Role
Value
Engineering leaders
Repeatable accessibility CI gate before release.
Compliance and procurement
Pipeline-attached evidence for EAA and EN 301 549 review.
Platform teams
Reusable task inputs that can be standardized across repositories.
+
Competitors
Deque axe DevTools, Microsoft Accessibility Insights, Siteimprove Azure DevOps connector, Evinced CI output, and older Marketplace accessibility checker tasks occupy adjacent CI accessibility surfaces.
+
Domains
Primary domain: accessibility CI gating. Adjacent domains: release governance, procurement evidence, EAA 2025 readiness, and enterprise DevOps standardization.
+
Technical connectors
The task invokes ariada scan, writes scan.json, emits Azure Pipelines logging commands, uploads the scan file, and publishes the output directory as a pipeline artifact.
Local distribution package is created with tfx extension create into dist/. Public distribution is blocked until the founder publishes through the Visual Studio Marketplace publisher account and shares it to an Azure DevOps organization.
+
Monetization
Sell as an enterprise CI channel: free thin task, paid Ariada reporting/support/policy packs for regulated teams that need auditable accessibility evidence.
Implemented: extension manifest, task manifest, Node task runner, local task-runner fixture, package validation, evidence HTML, embedded screenshot, and direct screenshot link. Not implemented: live Marketplace publication, Azure DevOps organization share, and live pipeline installation.
+
Blockers
Marketplace publish requires founder-owned Visual Studio Marketplace publisher access and Azure DevOps organization sharing/install rights. No live install was attempted.
+
Evidence
Local task-runner exit code: ${exitCode}. Raw scan JSON: ${link(evidenceHtml, scanJsonPath, 'ariada-output/scan.json')}.
+
+
What is Azure DevOps?
Azure DevOps is Microsoft's development platform; this S32 channel targets Azure Pipelines tasks that run during build and release jobs.
+
Why this is a separate Ariada channel
Azure Pipelines is a distinct enterprise CI surface from GitHub Actions, GitLab CI, Jenkins, and Bitbucket. A native Marketplace task lets Microsoft-standardized organizations run Ariada without maintaining copy-pasted shell snippets.
+
Roles: who pays / what value they buy
Role
Value
Engineering leaders
Repeatable accessibility CI gate before release.
Compliance and procurement
Pipeline-attached evidence for EAA and EN 301 549 review.
Platform teams
Reusable task inputs that can be standardized across repositories.
+
Competitors
Deque axe DevTools, Microsoft Accessibility Insights, Siteimprove Azure DevOps connector, Evinced CI output, and older Marketplace accessibility checker tasks occupy adjacent CI accessibility surfaces.
+
Domains
Primary domain: accessibility CI gating. Adjacent domains: release governance, procurement evidence, EAA 2025 readiness, and enterprise DevOps standardization.
+
Technical connectors
The task invokes ariada scan, writes scan.json, emits Azure Pipelines logging commands, uploads the scan file, and publishes the output directory as a pipeline artifact.
+
Screenshot
Embedded local report screenshot, with direct PNG link: ${link(evidenceHtml, screenshotPath, 'test-report/screenshot.png')}.
+
Distribution
Local distribution package is created with tfx extension create into dist/. Public distribution is blocked until the founder publishes through the Visual Studio Marketplace publisher account and shares it to an Azure DevOps organization.
+
Monetization
Sell as an enterprise CI channel: free thin task, paid Ariada reporting/support/policy packs for regulated teams that need auditable accessibility evidence.
Implemented: extension manifest, task manifest, Node task runner, local task-runner fixture, package validation, evidence HTML. Not implemented: live Marketplace publication and organization install.
Blockers
Marketplace publish requires founder-owned Visual Studio Marketplace publisher access and Azure DevOps organization sharing/install rights.
+
What is Azure DevOps?
Azure DevOps is Microsoft's development platform; this channel targets Azure Pipelines tasks that run during build and release jobs.
+
Why this is a separate Ariada channel
Microsoft-shop enterprises often standardize on Azure Pipelines rather than GitHub Actions or GitLab CI. A native task gives those teams a first-class procurement and pipeline surface for Ariada.
+
Roles: who pays / what value they buy
Role
Value
Engineering leaders
One CI gate that produces repeatable accessibility evidence before release.
Compliance and procurement
Evidence artifacts tied to a pipeline run for EAA and EN 301 549 review.
Platform teams
A reusable task with consistent inputs across many repositories.
+
Competitors
Deque axe DevTools, Microsoft Accessibility Insights, Siteimprove Azure DevOps connector, Evinced CI output, and older Marketplace accessibility checker tasks occupy adjacent CI accessibility surfaces.
+
Domains
Primary domain: accessibility CI gating. Adjacent domains: release governance, procurement evidence, EAA 2025 readiness, and enterprise DevOps standardization.
+
Technical connectors
The task invokes ariada scan, writes scan.json, emits Azure Pipelines logging commands, uploads the scan file, and publishes the output directory as a pipeline artifact.
Screenshot is captured after this report is rendered in the browser and stored as test-report/screenshot.png.
+
Distribution
Local distribution package is created with tfx extension create into dist/. Public distribution is blocked until the founder publishes through the Visual Studio Marketplace publisher account.
+
Monetization
Sell as an enterprise CI channel: free thin task, paid Ariada reporting/support/policy packs for regulated teams that need auditable accessibility evidence.
Implemented: extension manifest, task manifest, Node task runner, local task-runner fixture, package validation, evidence HTML. Not implemented: live Marketplace publication and organization install.
Blockers
Marketplace publish requires founder-owned Visual Studio Marketplace publisher access and Azure DevOps organization sharing/install rights.
+
What is Azure DevOps?
Azure DevOps is Microsoft's development platform; this channel targets Azure Pipelines tasks that run during build and release jobs.
+
Why this is a separate Ariada channel
Microsoft-shop enterprises often standardize on Azure Pipelines rather than GitHub Actions or GitLab CI. A native task gives those teams a first-class procurement and pipeline surface for Ariada.
+
Roles: who pays / what value they buy
Role
Value
Engineering leaders
One CI gate that produces repeatable accessibility evidence before release.
Compliance and procurement
Evidence artifacts tied to a pipeline run for EAA and EN 301 549 review.
Platform teams
A reusable task with consistent inputs across many repositories.
+
Competitors
Deque axe DevTools, Microsoft Accessibility Insights, Siteimprove Azure DevOps connector, Evinced CI output, and older Marketplace accessibility checker tasks occupy adjacent CI accessibility surfaces.
+
Domains
Primary domain: accessibility CI gating. Adjacent domains: release governance, procurement evidence, EAA 2025 readiness, and enterprise DevOps standardization.
+
Technical connectors
The task invokes ariada scan, writes scan.json, emits Azure Pipelines logging commands, uploads the scan file, and publishes the output directory as a pipeline artifact.
Screenshot is captured after this report is rendered in the browser and stored as test-report/screenshot.png.
+
Distribution
Local distribution package is created with tfx extension create into dist/. Public distribution is blocked until the founder publishes through the Visual Studio Marketplace publisher account.
+
Monetization
Sell as an enterprise CI channel: free thin task, paid Ariada reporting/support/policy packs for regulated teams that need auditable accessibility evidence.
+
diff --git a/integrations/azure-devops-ariada/test-report/runner-output.json b/integrations/azure-devops-ariada/test-report/runner-output.json
new file mode 100644
index 00000000..cbb8b0de
--- /dev/null
+++ b/integrations/azure-devops-ariada/test-report/runner-output.json
@@ -0,0 +1,7 @@
+{
+ "exitCode": 0,
+ "stdout": "Ariada Azure DevOps task running: /Users/pedro/adopta/.worktrees/adopta-s32-azure-devops/integrations/azure-devops-ariada/fixtures/mock-ariada-cli.mjs scan https://example.org/ariada-s32-fixture --severity-threshold serious --format json --output-dir /Users/pedro/adopta/.worktrees/adopta-s32-azure-devops/integrations/azure-devops-ariada/scan-evidence/ariada-output --timeout-ms 12000\nmock ariada wrote /Users/pedro/adopta/.worktrees/adopta-s32-azure-devops/integrations/azure-devops-ariada/scan-evidence/ariada-output/scan.json\n##vso[task.uploadfile]/Users/pedro/adopta/.worktrees/adopta-s32-azure-devops/integrations/azure-devops-ariada/scan-evidence/ariada-output/scan.json\n##vso[artifact.upload artifactname=ariada-output;]/Users/pedro/adopta/.worktrees/adopta-s32-azure-devops/integrations/azure-devops-ariada/scan-evidence/ariada-output\n##vso[task.complete result=Succeeded;]Ariada accessibility gate passed.\n",
+ "stderr": "",
+ "started": "2026-07-01T14:50:24.921Z",
+ "completed": "2026-07-01T14:50:25.145Z"
+}
\ No newline at end of file
diff --git a/integrations/azure-devops-ariada/test-report/screenshot.png b/integrations/azure-devops-ariada/test-report/screenshot.png
new file mode 100644
index 00000000..3fc70940
Binary files /dev/null and b/integrations/azure-devops-ariada/test-report/screenshot.png differ
diff --git a/integrations/azure-devops-ariada/vss-extension.json b/integrations/azure-devops-ariada/vss-extension.json
new file mode 100644
index 00000000..53dda900
--- /dev/null
+++ b/integrations/azure-devops-ariada/vss-extension.json
@@ -0,0 +1,45 @@
+{
+ "manifestVersion": 1,
+ "id": "ariada-azure-devops",
+ "name": "Ariada Accessibility Gate",
+ "version": "0.1.0",
+ "publisher": "ariada-org",
+ "public": false,
+ "description": "Azure Pipelines task that runs the Ariada accessibility CLI gate and uploads scan output as pipeline evidence.",
+ "categories": ["Azure Pipelines"],
+ "targets": [
+ {
+ "id": "Microsoft.VisualStudio.Services"
+ }
+ ],
+ "content": {
+ "details": {
+ "path": "overview.md"
+ }
+ },
+ "files": [
+ {
+ "path": "task",
+ "addressable": true
+ },
+ {
+ "path": "overview.md",
+ "addressable": true
+ }
+ ],
+ "contributions": [
+ {
+ "id": "ariada-accessibility-gate",
+ "type": "ms.vss-distributed-task.task",
+ "targets": ["ms.vss-distributed-task.tasks"],
+ "properties": {
+ "name": "task"
+ }
+ }
+ ],
+ "links": {
+ "learn": {
+ "uri": "https://github.com/ariada-org/ariada"
+ }
+ }
+}
diff --git a/integrations/backstage-ariada/README.md b/integrations/backstage-ariada/README.md
new file mode 100644
index 00000000..3c928667
--- /dev/null
+++ b/integrations/backstage-ariada/README.md
@@ -0,0 +1,20 @@
+# Ariada Backstage Plugin
+
+This stream is a Backstage frontend plugin surface for Ariada accessibility findings. It displays scan output from a hosted API or CI-produced report; it does not run a scanner.
+
+Official source checked: https://backstage.io/docs/frontend-system/building-plugins/index/, https://backstage.io/docs/tooling/package-metadata/, and https://backstage.io/docs/tutorials/package-role-migration/
+
+The current package ships the stable metadata and card-rendering contract. A live Backstage app can wrap `renderFindingsCard` in the host design system and fetch the same summary payload by catalog entity.
+
+## Local validation
+
+```bash
+pnpm exec tsc -p integrations/backstage-ariada/tsconfig.json
+pnpm exec eslint integrations/backstage-ariada/src integrations/backstage-ariada/tests
+pnpm exec vitest run integrations/backstage-ariada/tests/card.test.ts
+node integrations/backstage-ariada/scripts/validate-backstage.mjs
+```
+
+## Host blocker
+
+Running inside a live Backstage app requires `@backstage/create-app` output and host app wiring. That is a founder/platform-owner step; this package provides the plugin metadata and tested findings-card contract.
diff --git a/integrations/backstage-ariada/package.json b/integrations/backstage-ariada/package.json
new file mode 100644
index 00000000..cb0ac505
--- /dev/null
+++ b/integrations/backstage-ariada/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "@ariada-org/backstage-plugin-ariada",
+ "version": "0.1.0",
+ "description": "Backstage frontend plugin surface for Ariada accessibility findings.",
+ "license": "EUPL-1.2",
+ "type": "module",
+ "backstage": {
+ "role": "frontend-plugin"
+ },
+ "exports": {
+ ".": "./dist/index.js"
+ },
+ "scripts": {
+ "build": "tsc -p tsconfig.json",
+ "typecheck": "tsc -p tsconfig.json --noEmit",
+ "lint": "eslint src && node --check tests/card.test.mjs scripts/validate-backstage.mjs",
+ "test": "npm run build && node --test tests/*.test.mjs",
+ "validate": "node scripts/validate-backstage.mjs"
+ },
+ "devDependencies": {
+ "typescript": "^5.7.2"
+ },
+ "peerDependencies": {
+ "@backstage/core-plugin-api": ">=1.10.0",
+ "@backstage/plugin-catalog-react": ">=1.16.0",
+ "react": ">=18"
+ }
+}
diff --git a/integrations/backstage-ariada/scripts/validate-backstage.mjs b/integrations/backstage-ariada/scripts/validate-backstage.mjs
new file mode 100644
index 00000000..5f7869a9
--- /dev/null
+++ b/integrations/backstage-ariada/scripts/validate-backstage.mjs
@@ -0,0 +1,15 @@
+// SPDX-FileCopyrightText: 2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+import { readFile } from 'node:fs/promises';
+
+const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
+if (pkg.backstage?.role !== 'frontend-plugin') {
+ throw new Error('package.json must declare backstage.role=frontend-plugin');
+}
+for (const dep of ['@backstage/core-plugin-api', '@backstage/plugin-catalog-react', 'react']) {
+ if (!(dep in pkg.peerDependencies)) {
+ throw new Error(`package.json peerDependencies missing ${dep}`);
+ }
+}
+
+console.log('Backstage package shape OK: frontend-plugin role and peers present.');
diff --git a/integrations/backstage-ariada/src/index.ts b/integrations/backstage-ariada/src/index.ts
new file mode 100644
index 00000000..6dc20bf2
--- /dev/null
+++ b/integrations/backstage-ariada/src/index.ts
@@ -0,0 +1,50 @@
+// SPDX-FileCopyrightText: 2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+/**
+ *
+ */
+export interface AriadaFindingSummary {
+ readonly entityRef: string;
+ readonly score: number;
+ readonly status: 'pass' | 'warn' | 'fail';
+ readonly critical: number;
+ readonly serious: number;
+ readonly moderate: number;
+ readonly minor: number;
+ readonly reportUrl?: string;
+}
+
+export const backstagePluginId = 'ariada';
+
+/**
+ *
+ */
+export function summarizeForCatalogCard(summary: AriadaFindingSummary): string {
+ const total = summary.critical + summary.serious + summary.moderate + summary.minor;
+ return `${summary.entityRef}: ${summary.status.toUpperCase()} score ${summary.score}; ${total} findings.`;
+}
+
+/**
+ *
+ */
+export function renderFindingsCard(summary: AriadaFindingSummary): string {
+ const reportLink = summary.reportUrl
+ ? `Open report`
+ : 'No report URL';
+ return [
+ '',
+ `
Ariada accessibility
`,
+ `
${escapeHtml(summarizeForCatalogCard(summary))}
`,
+ `
Critical
${summary.critical}
Serious
${summary.serious}
Moderate
${summary.moderate}
Minor
${summary.minor}
`,
+ reportLink,
+ '',
+ ].join('');
+}
+
+function escapeHtml(value: string): string {
+ return value
+ .replaceAll('&', '&')
+ .replaceAll('<', '<')
+ .replaceAll('>', '>')
+ .replaceAll('"', '"');
+}
diff --git a/integrations/backstage-ariada/tests/card.test.mjs b/integrations/backstage-ariada/tests/card.test.mjs
new file mode 100644
index 00000000..298a791a
--- /dev/null
+++ b/integrations/backstage-ariada/tests/card.test.mjs
@@ -0,0 +1,31 @@
+// SPDX-FileCopyrightText: 2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+import assert from 'node:assert/strict';
+import test from 'node:test';
+
+import {
+ backstagePluginId,
+ renderFindingsCard,
+ summarizeForCatalogCard,
+} from '../dist/src/index.js';
+
+test('declares a stable plugin id', () => {
+ assert.equal(backstagePluginId, 'ariada');
+});
+
+test('renders a catalog-card summary from a mocked scan payload', () => {
+ const summary = {
+ entityRef: 'component:default/docs-site',
+ score: 91,
+ status: 'warn',
+ critical: 0,
+ serious: 1,
+ moderate: 2,
+ minor: 4,
+ reportUrl: 'https://ariada.example/reports/123',
+ };
+
+ assert.match(summarizeForCatalogCard(summary), /7 findings/u);
+ assert.match(renderFindingsCard(summary), /Ariada accessibility/u);
+ assert.match(renderFindingsCard(summary), /Open report/u);
+});
diff --git a/integrations/backstage-ariada/tsconfig.json b/integrations/backstage-ariada/tsconfig.json
new file mode 100644
index 00000000..4d9e51f4
--- /dev/null
+++ b/integrations/backstage-ariada/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": ".",
+ "outDir": "dist",
+ "types": ["node"]
+ },
+ "include": ["src/**/*.ts"]
+}
diff --git a/integrations/balsamiq-ariada/README.md b/integrations/balsamiq-ariada/README.md
new file mode 100644
index 00000000..6330540a
--- /dev/null
+++ b/integrations/balsamiq-ariada/README.md
@@ -0,0 +1,58 @@
+# Ariada Balsamiq Integration
+
+S126 is a documented Balsamiq export recipe plus a small Node wrapper. It does
+not add Balsamiq-side code because there is no Balsamiq plugin marketplace for a
+native Ariada plugin.
+
+## What It Does
+
+- Detects a Balsamiq HTML export folder, HTML file, or published Cloud URL.
+- Builds the `@ariada-org/cli scan` invocation for that rendered target.
+- Refuses PNG/PDF-only low-fidelity wireframes and points designers to the manual
+ accessibility checklist below.
+
+This integration is intentionally thin: all scanning remains in `@ariada-org/cli`.
+It does not implement contrast math, DOM analysis, or WCAG rule logic.
+
+## Usage
+
+```sh
+pnpm install
+pnpm run build
+node dist/cli.js --export-path fixtures/html-export --output-dir ariada-output --print
+node dist/cli.js --target-url https://example.test/balsamiq/prototype --output-dir ariada-output
+```
+
+## Manual Checklist For Low-Fidelity Exports
+
+PNG/PDF-only Balsamiq wireframes do not expose a DOM, ARIA tree, computed styles,
+or reliable final colors. For those exports, automated checks are out of scope.
+Use the wireframe review to record:
+
+- Reading order intent for WCAG 1.3.2 and 2.4.3.
+- Visible labels, helper text, and error-message intent for WCAG 2.4.6 and 3.3.2.
+- Target-size intent for tappable controls before implementation, mapped to WCAG
+ 2.5.8.
+
+Run the full Ariada scan once the wireframe is implemented as a page or when a
+Balsamiq Cloud HTML/published URL is available.
+
+## Local Gates
+
+```sh
+pnpm run lint
+pnpm run typecheck
+pnpm run test
+pnpm run validate
+```
+
+## Live-Host Blocker
+
+Blocked: Balsamiq does not provide a plugin marketplace for this distribution.
+The founder/listing step is to publish this recipe in an organization-owned
+examples repository and link it from Balsamiq workflow documentation.
+
+## Status
+
+Filler-tier channel. Useful only for HTML export or published URL scans; PNG/PDF
+wireframes remain manual guidance until rendered into HTML.
diff --git a/integrations/balsamiq-ariada/fixtures/html-export/index.html b/integrations/balsamiq-ariada/fixtures/html-export/index.html
new file mode 100644
index 00000000..3f964764
--- /dev/null
+++ b/integrations/balsamiq-ariada/fixtures/html-export/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Balsamiq Ariada fixture
+
+
+
+
pnpm run lint
+pnpm run typecheck
+pnpm run test
+pnpm run validate
+node dist/cli.js --export-path fixtures/html-export --output-dir ariada-output --print
+node packages/ariada-content-policy/dist/cli.js integrations/balsamiq-ariada/*
+
+ Result: passed. The wrapper detects an HTML export and builds the shared CLI
+ invocation. It rejects PNG/PDF-only low-fidelity exports with manual
+ checklist guidance. The content-policy gate passed for the new files.
+
+
+
+
+
RAG Status
+
+ RAG-first was requested when scripts/rag-query.sh exists. This
+ checkout does not contain that executable script, so no RAG query was run.
+
+
+
+
+
Host Blocker
+
+ No live marketplace screenshot is available because Balsamiq has no plugin
+ marketplace for this channel. Distribution is blocked on a founder-owned
+ examples repository or workflow documentation page that publishes the recipe.
+
+
+
+
diff --git a/integrations/balsamiq-ariada/scripts/validate-recipe.mjs b/integrations/balsamiq-ariada/scripts/validate-recipe.mjs
new file mode 100644
index 00000000..08b86dbb
--- /dev/null
+++ b/integrations/balsamiq-ariada/scripts/validate-recipe.mjs
@@ -0,0 +1,27 @@
+import { readFile } from 'node:fs/promises';
+
+const root = new URL('../', import.meta.url);
+const [pkg, recipe, readme] = await Promise.all([
+ readJson(new URL('package.json', root)),
+ readJson(new URL('recipe.json', root)),
+ readFile(new URL('README.md', root), 'utf8'),
+]);
+
+function readJson(url) {
+ return readFile(url, 'utf8').then((text) => JSON.parse(text));
+}
+
+if (pkg.name !== '@ariada-integrations/balsamiq-ariada') {
+ throw new Error('package name must remain the Balsamiq integration package');
+}
+if (recipe.scanner !== '@ariada-org/cli') {
+ throw new Error('recipe must invoke @ariada-org/cli');
+}
+for (const input of ['html-export-directory', 'html-file', 'published-http-url']) {
+ if (!recipe.supportedInputs.includes(input)) throw new Error(`recipe missing supported input: ${input}`);
+}
+for (const phrase of ['low-fidelity', 'no Balsamiq plugin marketplace', '@ariada-org/cli']) {
+ if (!readme.includes(phrase)) throw new Error(`README missing required phrase: ${phrase}`);
+}
+
+console.log('PASS balsamiq-ariada recipe');
diff --git a/integrations/balsamiq-ariada/src/cli.ts b/integrations/balsamiq-ariada/src/cli.ts
new file mode 100644
index 00000000..5f0aeaee
--- /dev/null
+++ b/integrations/balsamiq-ariada/src/cli.ts
@@ -0,0 +1,54 @@
+#!/usr/bin/env node
+import { spawnSync } from 'node:child_process';
+
+import { buildAriadaCliArgs, type BalsamiqScanConfig } from './index.js';
+
+function parseArgs(argv: string[]) {
+ const config: BalsamiqScanConfig = {};
+ let printOnly = false;
+
+ for (let index = 0; index < argv.length; index += 1) {
+ const arg = argv[index];
+ if (!arg) continue;
+ const readNext = () => {
+ const next = argv[index + 1];
+ if (!next) throw new Error(`Missing value for ${arg}`);
+ index += 1;
+ return next;
+ };
+
+ if (arg === '--print') {
+ printOnly = true;
+ } else if (arg === '--target-url') {
+ config.targetUrl = readNext();
+ } else if (arg === '--export-path') {
+ config.exportPath = readNext();
+ } else if (arg === '--output-dir') {
+ config.outputDir = readNext();
+ } else if (arg === '--severity-threshold') {
+ config.severityThreshold = readNext();
+ } else if (arg === '--format') {
+ const format = readNext();
+ if (format !== 'json' && format !== 'html' && format !== 'junit') {
+ throw new Error(`Unsupported format: ${format}`);
+ }
+ config.format = format;
+ } else if (!arg.startsWith('--') && !config.exportPath) {
+ config.exportPath = arg;
+ } else {
+ throw new Error(`Unknown or incomplete argument: ${arg}`);
+ }
+ }
+
+ return { config, printOnly };
+}
+
+const { config, printOnly } = parseArgs(process.argv.slice(2));
+const args = buildAriadaCliArgs(config);
+
+if (printOnly) {
+ console.log(['npx', '@ariada-org/cli', ...args].join(' '));
+} else {
+ const result = spawnSync('npx', ['@ariada-org/cli', ...args], { stdio: 'inherit' });
+ process.exit(result.status ?? 1);
+}
diff --git a/integrations/balsamiq-ariada/src/index.ts b/integrations/balsamiq-ariada/src/index.ts
new file mode 100644
index 00000000..5a35ad73
--- /dev/null
+++ b/integrations/balsamiq-ariada/src/index.ts
@@ -0,0 +1,78 @@
+import { existsSync, readdirSync, statSync } from 'node:fs';
+import { resolve } from 'node:path';
+
+export interface BalsamiqScanConfig {
+ exportPath?: string;
+ targetUrl?: string;
+ outputDir?: string;
+ severityThreshold?: string;
+ format?: 'json' | 'html' | 'junit';
+}
+
+export interface ResolvedBalsamiqTarget {
+ kind: 'published-url' | 'html-export';
+ target: string;
+}
+
+const htmlNames = ['index.html', 'export.html', 'prototype.html'];
+
+export function resolveBalsamiqTarget(config: BalsamiqScanConfig, cwd = process.cwd()): ResolvedBalsamiqTarget {
+ if (config.targetUrl) {
+ if (!/^https?:\/\/\S+$/u.test(config.targetUrl)) {
+ throw new Error('Balsamiq Ariada targetUrl must be an http(s) URL.');
+ }
+ return { kind: 'published-url', target: config.targetUrl };
+ }
+
+ if (!config.exportPath) {
+ throw new Error('Balsamiq Ariada requires --target-url or --export-path.');
+ }
+
+ const exportPath = resolve(cwd, config.exportPath);
+ if (!existsSync(exportPath)) {
+ throw new Error(`Balsamiq export path not found: ${exportPath}`);
+ }
+
+ const stat = statSync(exportPath);
+ if (stat.isFile() && /\.html?$/iu.test(exportPath)) {
+ return { kind: 'html-export', target: exportPath };
+ }
+
+ if (stat.isDirectory()) {
+ for (const name of htmlNames) {
+ const candidate = resolve(exportPath, name);
+ if (existsSync(candidate)) return { kind: 'html-export', target: candidate };
+ }
+ const firstHtml = readdirSync(exportPath)
+ .filter((name) => /\.html?$/iu.test(name))
+ .sort()[0];
+ if (firstHtml) return { kind: 'html-export', target: resolve(exportPath, firstHtml) };
+ }
+
+ throw new Error(
+ 'No HTML export found. PNG/PDF-only Balsamiq wireframes are too low-fidelity for automated Ariada scanning; use the manual checklist.',
+ );
+}
+
+export function buildAriadaCliArgs(config: BalsamiqScanConfig, cwd = process.cwd()): string[] {
+ const resolved = resolveBalsamiqTarget(config, cwd);
+ const args = [
+ 'scan',
+ resolved.target,
+ '--severity-threshold',
+ config.severityThreshold ?? 'serious',
+ '--format',
+ config.format ?? 'json',
+ ];
+
+ if (config.outputDir) args.push('--output-dir', config.outputDir);
+ return args;
+}
+
+export function manualChecklist() {
+ return [
+ 'Confirm reading order before visual polish; map the intended sequence to WCAG 1.3.2 and 2.4.3.',
+ 'Label every control and placeholder; map intent to WCAG 2.4.6 and 3.3.2 before implementation.',
+ 'Annotate target-size intent for tappable controls; map to WCAG 2.5.8 before handoff.',
+ ];
+}
diff --git a/integrations/balsamiq-ariada/tests/index.test.mjs b/integrations/balsamiq-ariada/tests/index.test.mjs
new file mode 100644
index 00000000..7f09a180
--- /dev/null
+++ b/integrations/balsamiq-ariada/tests/index.test.mjs
@@ -0,0 +1,42 @@
+import assert from 'node:assert/strict';
+import { dirname, resolve } from 'node:path';
+import test from 'node:test';
+import { fileURLToPath } from 'node:url';
+
+import { buildAriadaCliArgs, manualChecklist, resolveBalsamiqTarget } from '../dist/index.js';
+
+const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
+
+test('resolves a Balsamiq HTML export folder to index.html', () => {
+ const target = resolveBalsamiqTarget({ exportPath: 'fixtures/html-export' }, root);
+ assert.equal(target.kind, 'html-export');
+ assert.equal(target.target, resolve(root, 'fixtures/html-export/index.html'));
+});
+
+test('builds @ariada-org/cli scan arguments for published Cloud URLs', () => {
+ assert.deepEqual(
+ buildAriadaCliArgs({
+ targetUrl: 'https://example.test/balsamiq/prototype',
+ outputDir: 'ariada-output',
+ severityThreshold: 'moderate',
+ }),
+ [
+ 'scan',
+ 'https://example.test/balsamiq/prototype',
+ '--severity-threshold',
+ 'moderate',
+ '--format',
+ 'json',
+ '--output-dir',
+ 'ariada-output',
+ ],
+ );
+});
+
+test('rejects PNG-only exports and points to the manual checklist', () => {
+ assert.throws(
+ () => resolveBalsamiqTarget({ exportPath: 'fixtures/png-only' }, root),
+ /PNG\/PDF-only Balsamiq wireframes/u,
+ );
+ assert.ok(manualChecklist().some((item) => item.includes('WCAG 2.5.8')));
+});
diff --git a/integrations/balsamiq-ariada/tsconfig.json b/integrations/balsamiq-ariada/tsconfig.json
new file mode 100644
index 00000000..9147ab4d
--- /dev/null
+++ b/integrations/balsamiq-ariada/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "declaration": true,
+ "declarationMap": true,
+ "outDir": "dist",
+ "rootDir": "src",
+ "sourceMap": true,
+ "types": ["node"]
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["dist", "tests"]
+}
diff --git a/integrations/bitbucket-pipe-ariada/Dockerfile b/integrations/bitbucket-pipe-ariada/Dockerfile
new file mode 100644
index 00000000..5645c94b
--- /dev/null
+++ b/integrations/bitbucket-pipe-ariada/Dockerfile
@@ -0,0 +1,10 @@
+# SPDX-FileCopyrightText: 2026 Agonist Development AB
+# SPDX-License-Identifier: EUPL-1.2
+FROM node:22-bookworm-slim
+
+RUN npm install --global @ariada-org/cli
+
+COPY pipe/run.sh /usr/local/bin/ariada-bitbucket-pipe
+RUN chmod +x /usr/local/bin/ariada-bitbucket-pipe
+
+ENTRYPOINT ["ariada-bitbucket-pipe"]
diff --git a/integrations/bitbucket-pipe-ariada/README.md b/integrations/bitbucket-pipe-ariada/README.md
new file mode 100644
index 00000000..3d83cb3d
--- /dev/null
+++ b/integrations/bitbucket-pipe-ariada/README.md
@@ -0,0 +1,20 @@
+# Ariada Bitbucket Pipe
+
+This is the standalone, listing-grade Bitbucket Pipe. It is distinct from the earlier raw CI adapter: this directory contains the marketplace pipe layout (`Dockerfile`, `pipe.yml`, runner, and example pipeline).
+
+Official source checked: https://support.atlassian.com/bitbucket-cloud/docs/write-a-pipe-for-bitbucket-pipelines/
+
+The Pipe installs and runs `@ariada-org/cli`; it does not implement scan logic.
+
+## Local validation
+
+```bash
+yamllint -d relaxed pipe.yml bitbucket-pipelines.yml
+shellcheck pipe/run.sh
+node scripts/validate-pipe.mjs
+docker build -t ariada-bitbucket-pipe:test .
+```
+
+## Publication blocker
+
+Docker build requires a working Docker daemon. Publishing to the Bitbucket Pipes marketplace is a founder/listing step.
diff --git a/integrations/bitbucket-pipe-ariada/bitbucket-pipelines.yml b/integrations/bitbucket-pipe-ariada/bitbucket-pipelines.yml
new file mode 100644
index 00000000..f376ddc5
--- /dev/null
+++ b/integrations/bitbucket-pipe-ariada/bitbucket-pipelines.yml
@@ -0,0 +1,11 @@
+# SPDX-FileCopyrightText: 2026 Agonist Development AB
+# SPDX-License-Identifier: EUPL-1.2
+pipelines:
+ default:
+ - step:
+ name: Ariada accessibility gate
+ script:
+ - pipe: ariada/bitbucket-pipe-ariada:0.1.0
+ variables:
+ TARGET_URL: 'https://example.com'
+ FAIL_ON_SEVERITY: 'serious'
diff --git a/integrations/bitbucket-pipe-ariada/pipe.yml b/integrations/bitbucket-pipe-ariada/pipe.yml
new file mode 100644
index 00000000..72c43d90
--- /dev/null
+++ b/integrations/bitbucket-pipe-ariada/pipe.yml
@@ -0,0 +1,22 @@
+# SPDX-FileCopyrightText: 2026 Agonist Development AB
+# SPDX-License-Identifier: EUPL-1.2
+name: Ariada Accessibility Gate
+image: ariada/bitbucket-pipe-ariada:0.1.0
+description: Run the Ariada accessibility CLI from Bitbucket Pipelines.
+repository: https://github.com/ariada-org/bitbucket-pipe-ariada
+maintainer:
+ name: Agonist Development AB
+ website: https://ariada.org
+variables:
+ - name: TARGET_URL
+ default: ''
+ description: Absolute URL to scan.
+ - name: FAIL_ON_SEVERITY
+ default: 'serious'
+ allowed-values:
+ - 'minor'
+ - 'moderate'
+ - 'serious'
+ - 'critical'
+ - name: OUTPUT_DIR
+ default: 'ariada-output'
diff --git a/integrations/bitbucket-pipe-ariada/pipe/run.sh b/integrations/bitbucket-pipe-ariada/pipe/run.sh
new file mode 100755
index 00000000..8acb20c0
--- /dev/null
+++ b/integrations/bitbucket-pipe-ariada/pipe/run.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+# SPDX-FileCopyrightText: 2026 Agonist Development AB
+# SPDX-License-Identifier: EUPL-1.2
+set -euo pipefail
+
+TARGET_URL="${TARGET_URL:-}"
+FAIL_ON_SEVERITY="${FAIL_ON_SEVERITY:-serious}"
+OUTPUT_DIR="${OUTPUT_DIR:-ariada-output}"
+
+if [[ -z "$TARGET_URL" ]]; then
+ echo "TARGET_URL is required for the Ariada Bitbucket Pipe." >&2
+ exit 2
+fi
+
+mkdir -p "$OUTPUT_DIR"
+ariada scan "$TARGET_URL" --severity-threshold "$FAIL_ON_SEVERITY" --format json --output-dir "$OUTPUT_DIR"
diff --git a/integrations/bitbucket-pipe-ariada/scripts/validate-pipe.mjs b/integrations/bitbucket-pipe-ariada/scripts/validate-pipe.mjs
new file mode 100644
index 00000000..43a63ebc
--- /dev/null
+++ b/integrations/bitbucket-pipe-ariada/scripts/validate-pipe.mjs
@@ -0,0 +1,12 @@
+// SPDX-FileCopyrightText: 2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+import { readFile } from 'node:fs/promises';
+
+const pipe = await readFile(new URL('../pipe.yml', import.meta.url), 'utf8');
+for (const required of ['name:', 'image:', 'variables:', 'TARGET_URL', 'FAIL_ON_SEVERITY']) {
+ if (!pipe.includes(required)) {
+ throw new Error(`pipe.yml missing ${required}`);
+ }
+}
+
+console.log('Bitbucket Pipe metadata shape OK: name, image, and variables present.');
diff --git a/integrations/bubble-ariada/.gitignore b/integrations/bubble-ariada/.gitignore
new file mode 100644
index 00000000..3018b3a6
--- /dev/null
+++ b/integrations/bubble-ariada/.gitignore
@@ -0,0 +1 @@
+.tmp/
diff --git a/integrations/bubble-ariada/README.md b/integrations/bubble-ariada/README.md
new file mode 100644
index 00000000..81cbee00
--- /dev/null
+++ b/integrations/bubble-ariada/README.md
@@ -0,0 +1,51 @@
+# Ariada Bubble Plugin
+
+Bubble plugin scaffold for running Ariada hosted scans from a Bubble workflow.
+The plugin is intentionally thin: it does not reimplement scanner logic. It sends
+a published Bubble app URL to the Ariada hosted scan API and returns findings as
+Bubble action values.
+
+## What Is Included
+
+- `plugin/bubble-plugin.json` describes the Bubble API connector, workflow action,
+ returned values and result element.
+- `plugin/server-side-action.js` is the copyable server-side action shape for the
+ Bubble Plugin Editor.
+- `src/action.mjs` is the local Node implementation used by tests and evidence.
+- `scripts/run-e2e.mjs` starts a local mock hosted scan API and renders the action
+ result as a Bubble-like page for screenshot evidence.
+
+## Bubble Setup
+
+1. Open the Bubble plugin editor from a founder-owned Bubble account.
+2. Create a private plugin named `Ariada Accessibility Scan`.
+3. Add a server-side action named `Run Ariada scan`.
+4. Add private plugin keys for `ARIADA_SCAN_API_URL` and `ARIADA_API_TOKEN`.
+5. Copy the action logic from `plugin/server-side-action.js`.
+6. Add returned values matching `plugin/bubble-plugin.json`.
+7. Install the private plugin in a Bubble test app and call the action from a
+ workflow using the app's published URL.
+
+## Local Fixture
+
+```sh
+cd integrations/bubble-ariada
+npm run lint
+npm run validate
+npm test
+npm run test:e2e
+```
+
+The E2E flow uses a local hosted-API-compatible endpoint. It proves the Bubble
+action contract and evidence rendering, not Bubble editor import.
+
+## Marketplace Blocker
+
+Bubble marketplace submission is blocked until a founder-owned Bubble plugin
+editor account imports the action, the production Ariada hosted scan API is
+available, and a Bubble test app demonstrates the workflow inside Bubble.
+
+## Update
+
+- Author: Alexander Brichkin (Agonist Development AB)
+- Date: 2026-07-01
diff --git a/integrations/bubble-ariada/package.json b/integrations/bubble-ariada/package.json
new file mode 100644
index 00000000..da6db2e8
--- /dev/null
+++ b/integrations/bubble-ariada/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@ariada-integrations/bubble-ariada",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "description": "Bubble plugin scaffold and local evidence fixture for Ariada hosted scans.",
+ "license": "EUPL-1.2",
+ "scripts": {
+ "lint": "node --check src/action.mjs && node --check plugin/server-side-action.js && node --check scripts/validate-plugin.mjs && node --check scripts/run-e2e.mjs && node --check test/action.test.mjs",
+ "validate": "node scripts/validate-plugin.mjs",
+ "test": "node --test test/*.test.mjs",
+ "test:e2e": "node scripts/run-e2e.mjs"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+}
diff --git a/integrations/bubble-ariada/plugin/bubble-plugin.json b/integrations/bubble-ariada/plugin/bubble-plugin.json
new file mode 100644
index 00000000..2d4096be
--- /dev/null
+++ b/integrations/bubble-ariada/plugin/bubble-plugin.json
@@ -0,0 +1,56 @@
+{
+ "name": "Ariada Accessibility Scan",
+ "version": "0.1.0",
+ "platform": "Bubble",
+ "type": "plugin-scaffold",
+ "description": "Bubble plugin action that sends a published app URL to Ariada hosted scan API and returns scan findings to a Bubble workflow.",
+ "apiConnector": {
+ "name": "Ariada Hosted Scan",
+ "method": "POST",
+ "url": "https://api.ariada.org/v1/scans",
+ "authentication": "Private bearer token stored in Bubble plugin keys",
+ "requestBody": {
+ "url": "",
+ "domains": ["accessibility"],
+ "source": "bubble-plugin"
+ },
+ "responseShape": {
+ "ok": true,
+ "summary": {},
+ "findings": [],
+ "reportUrl": "https://app.ariada.org/scans/example"
+ }
+ },
+ "actions": [
+ {
+ "name": "Run Ariada scan",
+ "kind": "server_side_action",
+ "script": "plugin/server-side-action.js",
+ "inputs": [
+ { "key": "url_to_scan", "type": "text", "required": true },
+ { "key": "domains", "type": "list.text", "required": false },
+ { "key": "api_url", "type": "text", "required": false }
+ ],
+ "returnedValues": [
+ { "key": "ok", "type": "yes/no" },
+ { "key": "scanned_url", "type": "text" },
+ { "key": "findings_count", "type": "number" },
+ { "key": "serious_count", "type": "number" },
+ { "key": "summary_text", "type": "text" },
+ { "key": "findings_json", "type": "text" },
+ { "key": "report_url", "type": "text" },
+ { "key": "raw_json", "type": "text" }
+ ]
+ }
+ ],
+ "element": {
+ "name": "Ariada scan result",
+ "purpose": "Display the last scan summary and link to the retained Ariada report in a Bubble page.",
+ "states": ["summary_text", "findings_count", "serious_count", "report_url"]
+ },
+ "blockers": [
+ "Bubble plugin editor export/import must be completed in a founder-owned Bubble account.",
+ "Ariada hosted scan API endpoint and token must be production-ready before marketplace review.",
+ "Bubble marketplace submission is a founder-owned review step."
+ ]
+}
diff --git a/integrations/bubble-ariada/plugin/server-side-action.js b/integrations/bubble-ariada/plugin/server-side-action.js
new file mode 100644
index 00000000..7f1150b5
--- /dev/null
+++ b/integrations/bubble-ariada/plugin/server-side-action.js
@@ -0,0 +1,47 @@
+'use strict';
+
+module.exports = async function ariadaScanAction(properties, context) {
+ const targetUrl = properties.url_to_scan || properties.url || properties.website_url;
+ if (!targetUrl || !/^https?:\/\//u.test(targetUrl)) {
+ throw new Error('Ariada scan requires an http(s) URL.');
+ }
+
+ const endpoint =
+ properties.api_url ||
+ (context.keys && context.keys.ARIADA_SCAN_API_URL) ||
+ 'https://api.ariada.org/v1/scans';
+ const token = properties.api_token || (context.keys && context.keys.ARIADA_API_TOKEN) || '';
+ const response = await fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ 'content-type': 'application/json',
+ ...(token ? { authorization: `Bearer ${token}` } : {})
+ },
+ body: JSON.stringify({
+ url: targetUrl,
+ domains: properties.domains || ['accessibility'],
+ source: 'bubble-plugin'
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error(`Ariada hosted scan failed with HTTP ${response.status}.`);
+ }
+
+ const payload = await response.json();
+ const findings = Array.isArray(payload.findings)
+ ? payload.findings
+ : Object.values(payload.grid || {}).flatMap((domains) => Object.values(domains).flat());
+ const serious = findings.filter((finding) => ['serious', 'critical'].includes(finding.severity)).length;
+
+ return {
+ ok: serious === 0,
+ scanned_url: targetUrl,
+ findings_count: findings.length,
+ serious_count: serious,
+ summary_text: `Ariada found ${findings.length} finding(s), ${serious} serious or critical.`,
+ findings_json: JSON.stringify(findings),
+ report_url: payload.reportUrl || '',
+ raw_json: JSON.stringify(payload)
+ };
+};
diff --git a/integrations/bubble-ariada/scan-evidence/ariada-output/bubble-action-result.json b/integrations/bubble-ariada/scan-evidence/ariada-output/bubble-action-result.json
new file mode 100644
index 00000000..bb285679
--- /dev/null
+++ b/integrations/bubble-ariada/scan-evidence/ariada-output/bubble-action-result.json
@@ -0,0 +1,10 @@
+{
+ "ok": false,
+ "scanned_url": "http://127.0.0.1:62256/bubble-app",
+ "findings_count": 3,
+ "serious_count": 2,
+ "summary_text": "Ariada found 3 finding(s), 2 serious or critical.",
+ "findings_json": "[{\"id\":\"ariada/statement/page-link\",\"domain\":\"accessibility\",\"severity\":\"serious\",\"message\":\"Published Bubble page has no accessibility statement link.\"},{\"id\":\"ariada/image/text-alternative\",\"domain\":\"accessibility\",\"severity\":\"moderate\",\"message\":\"Hero image needs equivalent text.\"},{\"id\":\"ariada/form/label\",\"domain\":\"accessibility\",\"severity\":\"serious\",\"message\":\"Newsletter input is missing a visible label.\"}]",
+ "report_url": "https://app.ariada.ai/scans/bubble-local-fixture",
+ "raw_json": "{\"ok\":false,\"reportUrl\":\"https://app.ariada.ai/scans/bubble-local-fixture\",\"findings\":[{\"id\":\"ariada/statement/page-link\",\"domain\":\"accessibility\",\"severity\":\"serious\",\"message\":\"Published Bubble page has no accessibility statement link.\"},{\"id\":\"ariada/image/text-alternative\",\"domain\":\"accessibility\",\"severity\":\"moderate\",\"message\":\"Hero image needs equivalent text.\"},{\"id\":\"ariada/form/label\",\"domain\":\"accessibility\",\"severity\":\"serious\",\"message\":\"Newsletter input is missing a visible label.\"}]}"
+}
\ No newline at end of file
diff --git a/integrations/bubble-ariada/scan-evidence/ariada-output/hosted-api-fixture.json b/integrations/bubble-ariada/scan-evidence/ariada-output/hosted-api-fixture.json
new file mode 100644
index 00000000..3c8c7ac6
--- /dev/null
+++ b/integrations/bubble-ariada/scan-evidence/ariada-output/hosted-api-fixture.json
@@ -0,0 +1,24 @@
+{
+ "ok": false,
+ "reportUrl": "https://app.ariada.ai/scans/bubble-local-fixture",
+ "findings": [
+ {
+ "id": "ariada/statement/page-link",
+ "domain": "accessibility",
+ "severity": "serious",
+ "message": "Published Bubble page has no accessibility statement link."
+ },
+ {
+ "id": "ariada/image/text-alternative",
+ "domain": "accessibility",
+ "severity": "moderate",
+ "message": "Hero image needs equivalent text."
+ },
+ {
+ "id": "ariada/form/label",
+ "domain": "accessibility",
+ "severity": "serious",
+ "message": "Newsletter input is missing a visible label."
+ }
+ ]
+}
\ No newline at end of file
diff --git a/integrations/bubble-ariada/scan-evidence/bubble-action-preview.html b/integrations/bubble-ariada/scan-evidence/bubble-action-preview.html
new file mode 100644
index 00000000..5fab5f61
--- /dev/null
+++ b/integrations/bubble-ariada/scan-evidence/bubble-action-preview.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+Bubble Ariada action fixture
+
+
+
+
Bubble Ariada action fixture
+
Local Bubble-like page after the Run Ariada scan workflow action returns values.
+
Returned value
Value
ok
false
scanned_url
http://127.0.0.1:62256/bubble-app
findings_count
3
serious_count
2
summary_text
Ariada found 3 finding(s), 2 serious or critical.
report_url
https://app.ariada.ai/scans/bubble-local-fixture
+
Findings JSON returned to Bubble
+
[{"id":"ariada/statement/page-link","domain":"accessibility","severity":"serious","message":"Published Bubble page has no accessibility statement link."},{"id":"ariada/image/text-alternative","domain":"accessibility","severity":"moderate","message":"Hero image needs equivalent text."},{"id":"ariada/form/label","domain":"accessibility","severity":"serious","message":"Newsletter input is missing a visible label."}]
+
\ No newline at end of file
diff --git a/integrations/bubble-ariada/scan-evidence/command-output.txt b/integrations/bubble-ariada/scan-evidence/command-output.txt
new file mode 100644
index 00000000..277b9237
--- /dev/null
+++ b/integrations/bubble-ariada/scan-evidence/command-output.txt
@@ -0,0 +1,2 @@
+run Bubble server-side action url_to_scan=http://127.0.0.1:62256/bubble-app api_url=http://127.0.0.1:62256/ariada/scan
+Ariada found 3 finding(s), 2 serious or critical.
diff --git a/integrations/bubble-ariada/scan-evidence/command.exit b/integrations/bubble-ariada/scan-evidence/command.exit
new file mode 100644
index 00000000..573541ac
--- /dev/null
+++ b/integrations/bubble-ariada/scan-evidence/command.exit
@@ -0,0 +1 @@
+0
diff --git a/integrations/bubble-ariada/scan-evidence/result.html b/integrations/bubble-ariada/scan-evidence/result.html
new file mode 100644
index 00000000..52086dcd
--- /dev/null
+++ b/integrations/bubble-ariada/scan-evidence/result.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+Ariada Bubble scan evidence
+
+
+
+
Ariada Bubble scan evidence
+
Dash-style evidence report for S13 Bubble. The local E2E proves a Bubble plugin action contract against a hosted-API-compatible Ariada scan endpoint.
+
What is Bubble?
Topic
Bubble channel context
Platform
Bubble is a no-code web application builder used by founders, agencies and internal teams to build database-backed web apps through a visual editor, plugins, workflows and API connections.
How builders extend it
The normal extension surfaces are Bubble plugins, server-side actions, elements and API Connector calls. Builders expect configuration inside the Bubble editor, not package-manager or CLI setup.
Why accessibility evidence matters
Bubble apps can become customer portals, booking flows, dashboards or public-service forms. Once those apps face EU customers or regulated buyers, builders need repeatable accessibility evidence for release review.
What this report proves
This report proves the Ariada action contract and local evidence flow. It does not prove Bubble editor import or marketplace approval.
+
Why this is a separate Ariada channel
Reason
Implication for Ariada
Different user
A Bubble builder may not be a JavaScript developer and may not control deployment infrastructure. Ariada must surface as a workflow action and result values rather than as npm, CI YAML or a shell command.
Different runtime
Bubble plugins and API Connector calls execute inside Bubble-controlled server/client contexts. Heavy browser scanning belongs in Ariada hosted infrastructure; the plugin should pass a URL and display returned evidence.
Different buyer path
No-code agencies and product owners buy client delivery confidence, audit trail and retained reports. They do not buy a developer library.
Different blocker
The remaining blocker is not local code; it is Bubble editor import, real Bubble runtime permissions, production Ariada API credentials and marketplace submission.
+
Channel summary
Question
Answer
Channel
Bubble plugin / API connector for no-code app builders.
Why separate
Bubble users configure plugins, workflow actions and API connector calls rather than installing npm packages or running local CLI tools.
Current status
Local plugin action fixture implemented; Bubble editor import and marketplace review are blocked on a founder-owned Bubble account.
Scan semantics
Thin hosted API call compatible with Ariada scan results; no scanner logic is reimplemented in the plugin.
+
Channel culture fit and user preferences
Expectation
Bubble-specific answer
Fast local loop
Bubble builders expect editor configuration and workflow actions, not local Node or CLI ownership.
Heavy scanner placement
Browser scanning belongs in Ariada hosted API, with Bubble receiving structured action values.
Packaging
Private Bubble plugin first, marketplace plugin later; API Connector fallback for teams not ready for marketplace install.
Rejected path
Do not ask Bubble users to run the Ariada CLI or copy scanner code into client-side actions.
Community signal for returned-value shape confusion in plugin actions.
+
Pain-mining queries
Surface
Queries
Bubble forum
server-side action return values; API connector plugin action not showing; plugin marketplace review
Bubble docs/search
API Connector authentication, private plugin keys, Plugin Editor server-side actions
Marketplace
accessibility plugin, WCAG scan, compliance audit, site checker
No-signal searches
Ariada Bubble plugin; Bubble EAA scanner; Bubble WCAG evidence
+
Distribution and monetization next steps
Step
Owner / condition
Import private plugin into Bubble editor
Founder / Bubble account required.
Connect production Ariada hosted scan API
Ariada SaaS endpoint and token required.
Capture Bubble editor and Bubble app runtime screenshots
Founder or agent with account access.
Marketplace listing
Founder submission after private plugin evidence passes.
Paid layer
Hosted retention, baselines, exports and agency/client dashboards.
+
Command output
run Bubble server-side action url_to_scan=http://127.0.0.1:62256/bubble-app api_url=http://127.0.0.1:62256/ariada/scan
+Ariada found 3 finding(s), 2 serious or critical.
+
+
Action result JSON
{
+ "ok": false,
+ "scanned_url": "http://127.0.0.1:62256/bubble-app",
+ "findings_count": 3,
+ "serious_count": 2,
+ "summary_text": "Ariada found 3 finding(s), 2 serious or critical.",
+ "findings_json": "[{\"id\":\"ariada/statement/page-link\",\"domain\":\"accessibility\",\"severity\":\"serious\",\"message\":\"Published Bubble page has no accessibility statement link.\"},{\"id\":\"ariada/image/text-alternative\",\"domain\":\"accessibility\",\"severity\":\"moderate\",\"message\":\"Hero image needs equivalent text.\"},{\"id\":\"ariada/form/label\",\"domain\":\"accessibility\",\"severity\":\"serious\",\"message\":\"Newsletter input is missing a visible label.\"}]",
+ "report_url": "https://app.ariada.ai/scans/bubble-local-fixture",
+ "raw_json": "{\"ok\":false,\"reportUrl\":\"https://app.ariada.ai/scans/bubble-local-fixture\",\"findings\":[{\"id\":\"ariada/statement/page-link\",\"domain\":\"accessibility\",\"severity\":\"serious\",\"message\":\"Published Bubble page has no accessibility statement link.\"},{\"id\":\"ariada/image/text-alternative\",\"domain\":\"accessibility\",\"severity\":\"moderate\",\"message\":\"Hero image needs equivalent text.\"},{\"id\":\"ariada/form/label\",\"domain\":\"accessibility\",\"severity\":\"serious\",\"message\":\"Newsletter input is missing a visible label.\"}]}"
+}
+
\ No newline at end of file
diff --git a/integrations/bubble-ariada/scan-evidence/screenshots/bubble-action-result.png b/integrations/bubble-ariada/scan-evidence/screenshots/bubble-action-result.png
new file mode 100644
index 00000000..0dd4fab7
Binary files /dev/null and b/integrations/bubble-ariada/scan-evidence/screenshots/bubble-action-result.png differ
diff --git a/integrations/bubble-ariada/scripts/run-e2e.mjs b/integrations/bubble-ariada/scripts/run-e2e.mjs
new file mode 100644
index 00000000..4212b27f
--- /dev/null
+++ b/integrations/bubble-ariada/scripts/run-e2e.mjs
@@ -0,0 +1,326 @@
+import { createServer } from 'node:http';
+import { copyFile, mkdir, mkdtemp, readFile, readdir, writeFile } from 'node:fs/promises';
+import { existsSync, statSync } from 'node:fs';
+import { spawnSync } from 'node:child_process';
+import { join, resolve } from 'node:path';
+import { tmpdir } from 'node:os';
+import { runBubbleAriadaScan } from '../src/action.mjs';
+
+const root = resolve(import.meta.dirname, '..');
+const scanDir = resolve(root, 'scan-evidence');
+const testDir = resolve(root, 'test-report');
+const logsDir = resolve(testDir, 'logs');
+const outputDir = resolve(scanDir, 'ariada-output');
+const screenshotsDir = resolve(scanDir, 'screenshots');
+
+function esc(value) {
+ return String(value).replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char]);
+}
+
+function table(headers, rows) {
+ return `
${table(['Reason', 'Implication for Ariada'], separateChannelRows)}
+
Channel summary
${table(['Question', 'Answer'], reportRows)}
+
Channel culture fit and user preferences
${table(['Expectation', 'Bubble-specific answer'], [
+ ['Fast local loop', 'Bubble builders expect editor configuration and workflow actions, not local Node or CLI ownership.'],
+ ['Heavy scanner placement', 'Browser scanning belongs in Ariada hosted API, with Bubble receiving structured action values.'],
+ ['Packaging', 'Private Bubble plugin first, marketplace plugin later; API Connector fallback for teams not ready for marketplace install.'],
+ ['Rejected path', 'Do not ask Bubble users to run the Ariada CLI or copy scanner code into client-side actions.']
+ ])}
+
Recommended product solution
${table(['Decision', 'Recommendation'], [
+ ['Primary surface', 'Bubble server-side plugin action calling Ariada hosted scan API.'],
+ ['Fallback', 'Documented API Connector call using the same request and response shape.'],
+ ['Free vs paid', 'Keep private plugin/action scaffold free; sell hosted retention, baselines, exports and team dashboards.'],
+ ['Next native path', 'Founder imports plugin in Bubble editor, verifies return values, then prepares marketplace listing.']
+ ])}
+
Roles / who pays / what value they buy
${table(['Role', 'What value they buy', 'What we offer', 'Who pays', 'When we enter', 'Implemented / blockers'], roleRows)}
+
Кому что продаем: роли, hooks, кто платит и что уже готово
${table(['Role', 'What value they buy', 'What we offer', 'Who pays', 'When we enter', 'Implemented / blockers'], roleRows)}
+
${table(['Item', 'Status', 'Evidence or blocker'], implementationRows)}
+
Implemented vs missing
${table(['Item', 'Status', 'Evidence or blocker'], implementationRows)}
+
Technical connectors
${table(['Connector', 'Purpose', 'State'], [
+ ['Hosted API', 'Run Ariada scan and return JSON.', 'Mocked locally; production endpoint blocked.'],
+ ['Bubble server-side action', 'Expose scan as workflow step.', 'Implemented as copyable action shape.'],
+ ['API Connector fallback', 'Manual no-code configuration.', 'Manifest documents request and response.'],
+ ['Result element', 'Display summary/report link.', 'Described in plugin scaffold.']
+ ])}
+
E2E test adequacy
${table(['Question', 'Answer'], [
+ ['What it proves', 'Bubble action code calls a hosted scan endpoint, normalizes findings and renders returned values.'],
+ ['What it does not prove', 'It does not prove Bubble editor import, Bubble runtime permissions or marketplace acceptance.'],
+ ['Why acceptable now', 'S13 is gated on hosted API and Bubble account; local fixture is closest verifiable proof without fake marketplace claims.']
+ ])}
+
+ Screenshot of the local Bubble-like action result surface. Open PNG directly.
+ ${table(['Check', 'Finding'], [
+ ['Blank check', screenshotLooksValid(screenshot) ? 'PNG exists and is larger than 10 KB' : 'PNG missing or too small'],
+ ['Surface shown', 'The screenshot shows returned Bubble action values and findings JSON, not the Bubble editor itself.'],
+ ['Blocker classification', 'Bubble editor and marketplace screenshots remain external host blockers.']
+ ])}
+
This static fixture represents the shape of a Flutter web build when useful semantic output is present.
+
+
+
+
+
+
+
+
diff --git a/integrations/dart-flutter-ariada/lib/ariada.dart b/integrations/dart-flutter-ariada/lib/ariada.dart
new file mode 100644
index 00000000..e9425a40
--- /dev/null
+++ b/integrations/dart-flutter-ariada/lib/ariada.dart
@@ -0,0 +1,5 @@
+// SPDX-FileCopyrightText: 2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+
+export 'src/report.dart';
+export 'src/runner.dart';
diff --git a/integrations/dart-flutter-ariada/lib/src/report.dart b/integrations/dart-flutter-ariada/lib/src/report.dart
new file mode 100644
index 00000000..e8bd62bb
--- /dev/null
+++ b/integrations/dart-flutter-ariada/lib/src/report.dart
@@ -0,0 +1,85 @@
+// SPDX-FileCopyrightText: 2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+
+import 'dart:convert';
+import 'dart:io';
+
+const exitOk = 0;
+const exitViolations = 1;
+const exitInvalidArgs = 2;
+const exitRuntimeError = 3;
+
+const _severityRank = {
+ 'minor': 1,
+ 'moderate': 2,
+ 'serious': 3,
+ 'critical': 4,
+};
+
+bool isKnownSeverity(String severity) => _severityRank.containsKey(severity);
+
+int severityRank(String severity) => _severityRank[severity] ?? 2;
+
+class AriadaFinding {
+ AriadaFinding({
+ required this.ruleId,
+ required this.severity,
+ required this.message,
+ });
+
+ factory AriadaFinding.fromJson(Map json) {
+ return AriadaFinding(
+ ruleId: json['ruleId']?.toString() ?? 'unknown',
+ severity: json['severity']?.toString() ?? 'moderate',
+ message: json['message']?.toString() ?? '',
+ );
+ }
+
+ final String ruleId;
+ final String severity;
+ final String message;
+}
+
+class MultiDomainReport {
+ MultiDomainReport(this.findings);
+
+ factory MultiDomainReport.fromJsonString(String raw) {
+ final decoded = jsonDecode(raw);
+ if (decoded is! Map) {
+ throw const FormatException('Ariada report root must be an object');
+ }
+ final grid = decoded['grid'];
+ if (grid is! Map) {
+ throw const FormatException('Ariada report is missing grid');
+ }
+
+ final findings = [];
+ for (final byDomain in grid.values) {
+ if (byDomain is! Map) continue;
+ for (final domainFindings in byDomain.values) {
+ if (domainFindings is! List) continue;
+ for (final finding in domainFindings) {
+ if (finding is Map) {
+ findings.add(
+ AriadaFinding.fromJson(Map.from(finding)),
+ );
+ }
+ }
+ }
+ }
+ return MultiDomainReport(findings);
+ }
+
+ factory MultiDomainReport.fromFile(File file) {
+ return MultiDomainReport.fromJsonString(file.readAsStringSync());
+ }
+
+ final List findings;
+
+ int countAtOrAbove(String threshold) {
+ final minimum = severityRank(threshold);
+ return findings
+ .where((finding) => severityRank(finding.severity) >= minimum)
+ .length;
+ }
+}
diff --git a/integrations/dart-flutter-ariada/lib/src/runner.dart b/integrations/dart-flutter-ariada/lib/src/runner.dart
new file mode 100644
index 00000000..0c8b3733
--- /dev/null
+++ b/integrations/dart-flutter-ariada/lib/src/runner.dart
@@ -0,0 +1,218 @@
+// SPDX-FileCopyrightText: 2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+
+import 'report.dart';
+
+sealed class ScanTarget {
+ const ScanTarget();
+}
+
+class UrlTarget extends ScanTarget {
+ const UrlTarget(this.url);
+
+ final Uri url;
+}
+
+class StaticDirTarget extends ScanTarget {
+ const StaticDirTarget(this.path);
+
+ final Directory path;
+}
+
+class AriadaOptions {
+ AriadaOptions({
+ required this.target,
+ required this.outputDir,
+ required this.ariadaBin,
+ required this.severityThreshold,
+ this.allowPrivate = false,
+ this.domains = const [],
+ });
+
+ final ScanTarget target;
+ final Directory outputDir;
+ final String ariadaBin;
+ final String severityThreshold;
+ final bool allowPrivate;
+ final List domains;
+
+ void validate() {
+ if (ariadaBin.trim().isEmpty) {
+ throw const FormatException('provide a non-empty Ariada CLI command');
+ }
+ if (!isKnownSeverity(severityThreshold)) {
+ throw FormatException('unknown severity threshold $severityThreshold');
+ }
+ final targetValue = target;
+ if (targetValue is UrlTarget) {
+ if (!targetValue.url.hasScheme ||
+ !['http', 'https'].contains(targetValue.url.scheme)) {
+ throw const FormatException('provide an http(s) URL or --static-dir');
+ }
+ } else if (targetValue is StaticDirTarget && !targetValue.path.existsSync()) {
+ throw FormatException(
+ 'static output dir does not exist: ${targetValue.path.path}',
+ );
+ }
+ }
+}
+
+class CommandResult {
+ CommandResult({
+ required this.stdout,
+ required this.stderr,
+ required this.exitCode,
+ });
+
+ final String stdout;
+ final String stderr;
+ final int exitCode;
+}
+
+abstract interface class CommandRunner {
+ Future run(String executable, List arguments);
+}
+
+class ProcessCommandRunner implements CommandRunner {
+ const ProcessCommandRunner();
+
+ @override
+ Future run(String executable, List arguments) async {
+ try {
+ final result = await Process.run(executable, arguments);
+ return CommandResult(
+ stdout: result.stdout.toString(),
+ stderr: result.stderr.toString(),
+ exitCode: result.exitCode,
+ );
+ } on Object catch (error) {
+ return CommandResult(
+ stdout: '',
+ stderr: error.toString(),
+ exitCode: exitRuntimeError,
+ );
+ }
+ }
+}
+
+Future runAriadaScan(
+ AriadaOptions options,
+ CommandRunner runner, {
+ IOSink? stdoutSink,
+ IOSink? stderrSink,
+}) async {
+ options.validate();
+ options.outputDir.createSync(recursive: true);
+
+ StaticServer? server;
+ final targetUrl = switch (options.target) {
+ UrlTarget(:final url) => url.toString(),
+ StaticDirTarget(:final path) => (server = await StaticServer.start(path)).url,
+ };
+
+ try {
+ final result = await runner.run(
+ options.ariadaBin,
+ buildAriadaArguments(options, targetUrl),
+ );
+ stdoutSink?.write(result.stdout);
+ stderrSink?.write(result.stderr);
+
+ final reportFile = File(p.join(options.outputDir.path, 'multi-domain-report.json'));
+ if (!reportFile.existsSync()) {
+ return result.exitCode == exitOk ? exitRuntimeError : _normalizeExit(result.exitCode);
+ }
+ final report = MultiDomainReport.fromFile(reportFile);
+ final count = report.countAtOrAbove(options.severityThreshold);
+ if (count > 0) {
+ stdoutSink?.writeln(
+ 'ariada: $count finding(s) at or above ${options.severityThreshold}',
+ );
+ return exitViolations;
+ }
+ stdoutSink?.writeln('ariada: no findings at or above ${options.severityThreshold}');
+ return exitOk;
+ } finally {
+ await server?.close();
+ }
+}
+
+List buildAriadaArguments(AriadaOptions options, String targetUrl) {
+ final args = [
+ 'scan',
+ targetUrl,
+ '--format',
+ 'both',
+ '--output-dir',
+ options.outputDir.path,
+ '--severity-threshold',
+ options.severityThreshold,
+ ];
+ if (options.domains.isNotEmpty) {
+ args.addAll(['--domains', options.domains.join(',')]);
+ }
+ if (options.allowPrivate || options.target is StaticDirTarget) {
+ args.add('--allow-private');
+ }
+ return args;
+}
+
+int _normalizeExit(int exitCode) {
+ if ([exitOk, exitViolations, exitInvalidArgs].contains(exitCode)) {
+ return exitCode;
+ }
+ return exitRuntimeError;
+}
+
+class StaticServer {
+ StaticServer._(this._server, this._root);
+
+ final HttpServer _server;
+ final Directory _root;
+
+ String get url => 'http://${_server.address.host}:${_server.port}/';
+
+ static Future start(Directory root) async {
+ final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
+ final staticServer = StaticServer._(server, root);
+ unawaited(staticServer._listen());
+ return staticServer;
+ }
+
+ Future _listen() async {
+ await for (final request in _server) {
+ final path = _resolvePath(request.uri.path);
+ if (path == null || !File(path).existsSync()) {
+ request.response.statusCode = HttpStatus.notFound;
+ await request.response.close();
+ continue;
+ }
+ request.response.headers.contentType = _contentType(path);
+ await File(path).openRead().pipe(request.response);
+ }
+ }
+
+ String? _resolvePath(String urlPath) {
+ final normalized = p.normalize(urlPath == '/' ? 'index.html' : urlPath.substring(1));
+ if (p.isAbsolute(normalized) || normalized.startsWith('..')) return null;
+ return p.join(_root.path, normalized);
+ }
+
+ Future close() => _server.close(force: true);
+}
+
+ContentType _contentType(String path) {
+ return switch (p.extension(path)) {
+ '.html' => ContentType.html,
+ '.css' => ContentType('text', 'css'),
+ '.js' => ContentType('application', 'javascript'),
+ '.json' => ContentType.json,
+ '.svg' => ContentType('image', 'svg+xml'),
+ _ => ContentType.binary,
+ };
+}
diff --git a/integrations/dart-flutter-ariada/pubspec.yaml b/integrations/dart-flutter-ariada/pubspec.yaml
new file mode 100644
index 00000000..48d8ddd3
--- /dev/null
+++ b/integrations/dart-flutter-ariada/pubspec.yaml
@@ -0,0 +1,26 @@
+name: ariada
+description: Thin Dart and Flutter web adapter for the shared Ariada scanner CLI.
+version: 0.1.0
+repository: https://github.com/ariada-org/ariada
+issue_tracker: https://github.com/ariada-org/ariada/issues
+
+environment:
+ sdk: ">=3.4.0 <4.0.0"
+
+executables:
+ scan:
+
+dependencies:
+ args: ^2.5.0
+ path: ^1.9.0
+
+dev_dependencies:
+ lints: ^4.0.0
+ test: ^1.25.0
+
+topics:
+ - accessibility
+ - flutter
+ - web
+ - compliance
+ - wcag
diff --git a/integrations/dart-flutter-ariada/scan-evidence/ariada-output/multi-domain-report.json b/integrations/dart-flutter-ariada/scan-evidence/ariada-output/multi-domain-report.json
new file mode 100644
index 00000000..b140df13
--- /dev/null
+++ b/integrations/dart-flutter-ariada/scan-evidence/ariada-output/multi-domain-report.json
@@ -0,0 +1,34 @@
+{
+ "sites": [
+ "file://fixtures/flutter-web-html-renderer/build/web/index.html"
+ ],
+ "domains": [
+ "accessibility"
+ ],
+ "grid": {
+ "file://fixtures/flutter-web-html-renderer/build/web/index.html": {
+ "accessibility": [
+ {
+ "ruleId": "ariada/images/alt-text",
+ "severity": "serious",
+ "message": "Fixture image has no alt text."
+ },
+ {
+ "ruleId": "ariada/forms/label",
+ "severity": "serious",
+ "message": "Email input has no associated label."
+ },
+ {
+ "ruleId": "ariada/buttons/name",
+ "severity": "moderate",
+ "message": "Button has no accessible name."
+ },
+ {
+ "ruleId": "ariada/statement/page-link-from-footer",
+ "severity": "moderate",
+ "message": "Accessibility statement link is missing from the footer."
+ }
+ ]
+ }
+ }
+}
diff --git a/integrations/dart-flutter-ariada/scan-evidence/command.exit b/integrations/dart-flutter-ariada/scan-evidence/command.exit
new file mode 100644
index 00000000..c92fa5c2
--- /dev/null
+++ b/integrations/dart-flutter-ariada/scan-evidence/command.exit
@@ -0,0 +1 @@
+HOST_BLOCKED_NO_DART_FLUTTER
diff --git a/integrations/dart-flutter-ariada/scan-evidence/command.log b/integrations/dart-flutter-ariada/scan-evidence/command.log
new file mode 100644
index 00000000..672a845a
--- /dev/null
+++ b/integrations/dart-flutter-ariada/scan-evidence/command.log
@@ -0,0 +1,5 @@
+$ dart run ariada:scan --static-dir fixtures/flutter-web-html-renderer/build/web --domains accessibility --severity-threshold moderate --output-dir scan-evidence/ariada-output --ariada-bin ariada-stub
+Host blocker: this workstation has no Dart or Flutter executable, so dart pub get, dart analyze, dart test, dart format, dart pub publish --dry-run, and a real flutter build web run could not execute locally.
+Adapter contract note: --static-dir serves the fixture on loopback and forwards --allow-private to the shared @ariada-org/cli; served local URLs require the explicit --allow-private wrapper flag.
+Validated instead: package source structure, fixture output path, synthetic shared-CLI JSON parser contract, screenshot capture, screenshot dimensions, nonblank pixels, and Dash-plus report audit.
+The stub evidence models the expected shared @ariada-org/cli output and demonstrates the adapter contract without reimplementing scanner rules.
diff --git a/integrations/dart-flutter-ariada/scan-evidence/result.html b/integrations/dart-flutter-ariada/scan-evidence/result.html
new file mode 100644
index 00000000..996562b6
--- /dev/null
+++ b/integrations/dart-flutter-ariada/scan-evidence/result.html
@@ -0,0 +1,453 @@
+
+
+S106 Dart/Flutter web Ariada evidence report
+
+
+
+
+
S106 — Dart/Flutter pub package for Flutter web
+
Dash-style channel evidence report for a thin Dart adapter around the shared Ariada scanner CLI. The report is intentionally explicit about the Flutter web renderer caveat, host blockers, tested surface classification, monetization path, and community-review evidence.
+ Evidence classification: tested host surface link plus documented host blocker.
+
+
+
+
+
What is Flutter web?
+
What is Flutter web? Flutter web is the browser-targeted deployment mode for Flutter applications. For Ariada it is not simply “another Dart app”: the scannable surface is the generated web output after Flutter rendering choices, semantics, assets, and shell HTML have been applied.
+
Why this is a separate Ariada channel: pub.dev is the normal package distribution route for Dart and Flutter developers, and dart run package:executable is a recognizable way to add explicit tooling. Flutter web also has a unique accessibility problem: some output can expose a semantics-backed DOM, while canvas-heavy rendering can make conventional DOM scanners less complete.
+
Source reliability: official Dart, Flutter, W3C, EU, pub.dev and standards pages are high-reliability primary or standards sources. GitHub issues, Reddit, Stack Overflow and Hacker News are community-review evidence only: they identify pain language and repeated objections, not market-size proof or legal truth.
+
+
Why this is a separate Ariada channel
Flutter web deserves its own channel because Dart/Flutter teams discover tools through pub.dev and expect Dart-shaped commands. The audience is smaller than raw Flutter adoption: only Flutter web outputs with useful web semantics can be meaningfully scanned by a DOM-oriented Ariada run.
Reason
S106 answer
pub.dev distribution
Package exposes dart run ariada:scan, matching the S106 handoff contract.
+
Flutter renderer split
Report explicitly distinguishes HTML/semantic output from CanvasKit/Skwasm-heavy output.
+
Compliance evidence
Ariada artifacts attach to built web output, not mobile widget source.
+
Channel culture fit
Expectation
S106 fit
Fast local/dev loop
Dart teams accept `dart format`, `dart analyze`, `dart test`, `flutter test`, package lints, and small explicit `dart run` tools. Ariada belongs as an explicit scan command, not hidden inside unit tests.
+
CI/release loop
Browser-driven DOM scans, Node-based shared CLI setup, screenshot capture, and retained artifacts are acceptable in CI, nightly, release, and compliance workflows.
+
Rejected pattern
A surprise browser/Node scan inside every Flutter widget test would feel foreign and slow. CanvasKit output also makes conventional DOM checks incomplete.
+
Packaging expectation
pub.dev package, `bin/scan.dart`, `executables: scan`, `dart run ariada:scan`, optional `dart pub global activate`, and copy-paste CI examples.
+
Foreign dependency
The scanner remains the shared `@ariada-org/cli`, currently distributed via Node/npm. This Dart package is not a native scanner and should not claim to be one.
+
Workflow placement
Best placement is after `flutter build web`, in pre-merge CI, nightly evidence, release gates, procurement packets, and hosted fleet scans.
+
Recommended product solution
Layer
Decision
Primary entrypoint
`dart run ariada:scan --static-dir build/web` after a Flutter web build. It is explicit, pub.dev-shaped, and easy to add to CI.
+
Fallback entrypoint
Reusable GitHub Action or Docker image that installs Dart/Flutter, shared Ariada CLI, browser runtime, and uploads artifacts.
Retained evidence, signed exports, baseline policies, exception workflow, team dashboards, and domain packs.
+
What developers should not own
Do not make each Flutter team hand-assemble Playwright caches, npm global installs, screenshot validation, evidence signing, or archival retention.
+
Next idiomatic version
A verified pub.dev package, Action/Docker recipe, Flutter SDK example app, HTML-renderer guidance, and hosted upload path.
+
Roles: who pays / what value they buy
The exact mandated table follows. It maps hooks, payer, buying moment, and implementation state.
+
Кому что продаем: роли, hooks, кто платит и что уже готово
Role
Hook
Who pays
Buying moment
Implemented state
Flutter web developer
Runs one Dart-shaped command after `flutter build web`, without learning scanner internals.
Usually not payer; creates the adoption pull by proving fixture and CI usefulness.
Local release check or CI job for public Flutter web bundles.
MVP bridge implemented; real Flutter SDK run is host-blocked here.
+
Mobile-first Flutter team lead
Learns when web accessibility evidence is feasible and when CanvasKit output limits DOM scanning.
Influences platform budget; buys only when web becomes customer-facing.
Before converting an internal Flutter app into a public web surface.
Caveat documented; native Flutter semantics checks are not implemented here.
+
Platform or CI owner
Gets repeatable JSON/log/screenshot/HTML artifacts with standard exit codes.
Pays from platform/tooling budget once evidence becomes a release gate.
Pre-merge, nightly, or release artifact generation.
Wrapper contract exists; reusable GitHub Action/Docker path remains next work.
+
Accessibility reviewer
Receives direct fixture screenshot, raw JSON, command log, and report instead of a screenshot-only claim.
Influences compliance purchase and remediation priority.
Review of EAA/WCAG evidence for a Flutter web release.
Evidence path implemented against representative fixture; real app proof pending Flutter SDK.
+
Security or compliance owner
Can later combine accessibility, privacy/GDPR, security, performance, sustainability and AI/compliance evidence.
Enterprise compliance/legal budget when retained evidence is required.
Procurement, regulated release, or supplier acceptance.
Accessibility evidence exists; multi-domain hosted retention is not implemented.
+
Public-sector supplier
Needs EN 301 549/EAA evidence when a Flutter web bundle is delivered as a public service.
Project or contract budget.
Before acceptance testing or remedial sign-off.
Local evidence bundle is available; signed exports and audit retention are future paid layer.
+
Implemented vs not implemented
Capability
Status
Evidence
pub package skeleton
IMPLEMENTED
`pubspec.yaml` defines a Dart package, executable `scan`, metadata, lints, args/path dependencies, and test dependency.
+
Dart entrypoint
IMPLEMENTED
`bin/scan.dart` parses `--url`, `--static-dir`, `--domains`, `--severity-threshold`, `--output-dir`, and `--ariada-bin`.
+
Shared CLI invocation
IMPLEMENTED
`Process.run` invokes `@ariada-org/cli` via `ariada` or `ARIADA_BIN`. No scanner rule is implemented in Dart.
+
JSON parser
IMPLEMENTED
`MultiDomainReport` reads Ariada `grid` output and counts findings at or above the configured threshold.
+
Static-dir bridge
IMPLEMENTED
The wrapper can serve `build/web` on loopback and pass the generated URL to the shared CLI.
+
Representative HTML fixture
IMPLEMENTED
Fixture models a DOM-scannable Flutter web HTML-renderer output with known defects.
+
CanvasKit caveat fixture
IMPLEMENTED
Second fixture documents a canvas-heavy output shape where DOM scanners have limited signal.
+
Unit tests
WRITTEN, HOST-BLOCKED
`test/report_test.dart` and `test/runner_test.dart` cover parsing and stub CLI contract, but Dart is not installed here.
+
Dart analyze/format/pub dry-run
HOST BLOCKER
Blocked because neither `dart` nor `flutter` exists on this workstation path.
+
Real Flutter build
HOST BLOCKER
Blocked by missing Flutter SDK and renderer-specific build support. The fixture proves evidence shape, not full Flutter runtime coverage.
+
pub.dev publication
HUMAN BLOCKER
Requires Google account, verified publisher, final package-name decision, and release credentials.
+
Hosted retention and signing
NOT IMPLEMENTED
Local artifacts are generated; paid signed exports and retention belong to hosted Ariada.
+
Scanner rules
NOT IMPLEMENTED HERE
Accessibility, security, privacy/GDPR, performance, reliability, sustainability, SEO/AIEO/GEO, legal notices, localization/i18n, data provenance, and AI/compliance rules remain in shared Ariada domains.
+
Ariada core used
The Dart code invokes the shared CLI and parses the shared report format. It does not implement WCAG checks, browser automation, privacy detection, security checks, performance scoring, sustainability scoring, SEO/AIEO/GEO analysis, legal notice checks, localization checks, data-provenance checks, or AI/compliance checks.
Connector
Status
@ariada-org/cli
External scanner executable invoked by Dart wrapper.
+
multi-domain-report.json
Shared JSON contract parsed by Dart wrapper.
+
Ariada domain packages
Used only through CLI output.
+
Technical connectors
Connector
Current path
Dart pub executable
pubspec.yaml + bin/scan.dart
+
Flutter web static output
--static-dir build/web loopback server
+
Live URL
--url http://127.0.0.1:8080/
+
Shared CLI override
ARIADA_BIN or --ariada-bin
+
CI artifacts
scan-evidence/ariada-output, command log, screenshots, HTML report
+
Future GitHub Action
Should install Dart/Flutter, Node CLI, browser runtime, then upload artifacts.
+
Tested surface
The tested host surface screenshot is the representative Flutter web/static output fixture. The scan-result preview screenshot is a rendered evidence summary. A report-only screenshot would be supplemental only; it is not used as the sole visual evidence.
Secondary evidence; shows parsed findings from Ariada JSON.
+
result.html screenshot evidence
linked PNG plus host blocker
Not counted alone; links the tested-host screenshot and documents the host blocker.
+
Visual evidence review
Visual evidence classification: tested-host-surface, scan-result preview, and report links are intentionally separated. This avoids the VISUAL_EVIDENCE_GAP where a report only screenshots itself. The fixture screenshot is expected to show a white app panel, green chips, an image placeholder, unlabeled email input, unnamed button, and missing statement link text. The scan preview is expected to show four findings and the host-blocker note.
Loopback server bridges build/web style output to the shared CLI.
+
CanvasKit caveat fixture
Documents low-DOM output as a limitation rather than pretending coverage.
+
Screenshot validation
Dimensions and nonblank pixels checked locally.
+
Verification and test adequacy
Test adequacy is partial because the host lacks Dart and Flutter. The source includes Dart tests and package metadata, but local execution could not prove analyzer, format, pub resolution, or dart test. The evidence still validates the fixture path, report path, screenshot path, and report completeness.
Gate
Status
command -v dart
not found
+
command -v flutter
not found
+
node scripts/validate-screenshots.mjs
locally runnable and required before commit
+
Dash-plus audit
locally runnable with root audit script and Dash baseline
+
Blockers
Blocker
Exact owner/action
Dart SDK missing
Install Dart SDK before running `dart pub get`, `dart analyze`, `dart test`, `dart format`, and `dart pub publish --dry-run`.
+
Flutter SDK missing
Install Flutter before generating a real `flutter build web --web-renderer html` fixture.
+
pub.dev publication
Founder/release coordinator must approve package name, Google account, verified publisher, and credentials.
+
CanvasKit/Skwasm coverage
Requires native Flutter semantics/testing path or explicit limitation for DOM scanners.
+
Shared CLI distribution
Dart users still need npm/global CLI, CI Action, Docker image, or hosted worker to hide Node/browser setup.
+
Domain map
Domain
State
S106 interpretation
Accessibility
implemented for fixture
Current evidence uses missing image alt, missing input label, unnamed button, and missing accessibility-statement link. Flutter semantics can expose accessible DOM, but CanvasKit/Skwasm shapes require caution.
+
Security
planned
Flutter web releases still need CSP, security headers, dependency and third-party script posture. Ariada should pass the shared security domain through, not implement it in Dart.
+
Privacy/GDPR
planned
Cookie notices, analytics tags, consent links, and data minimization claims belong in shared privacy checks. Flutter teams often embed analytics SDKs at the web shell.
+
Performance
planned
Flutter web payload size, CanvasKit assets, WebAssembly, and initial render timing are key buying hooks. Use shared performance domain when D07 matures.
+
Reliability
planned
Release evidence should include route availability, blank-screen risk, asset load failure, and broken link checks for web bundles.
+
Sustainability
planned
Flutter web can ship large binary/runtime assets. Domain should measure transfer size and third-party cost via shared Ariada logic.
+
SEO/AIEO/GEO
planned
Canvas-heavy output can be weak for public search and AI citation. HTML shell metadata, structured content, and crawlability must be tested separately.
+
Legal notices
planned
Footer links for accessibility statement, privacy policy, imprint/legal notice, and terms are important for EU public websites and procurement review.
+
Localization/i18n
planned
Flutter apps need `lang`, translated labels, locale-specific legal notices, and bidirectional text checks.
+
Data provenance
planned
Useful when Flutter web surfaces dashboards, datasets, or generated content that need source lineage.
+
AI/compliance
planned
Future checks can verify AI disclosure, EU AI Act notices, and generated-content transparency where relevant.
+
Native Flutter semantics
blocked
Ariada does not inspect Flutter widget trees or semantics tests today; this would require a separate Flutter-native plugin path.
+
Domain map: accessibility, security, privacy/GDPR, performance, reliability, sustainability, SEO/AIEO/GEO, legal notices, localization/i18n, data provenance, AI/compliance where relevant
This heading is deliberately explicit because S106 is a cross-domain release-evidence channel. Accessibility is implemented in the fixture; every other domain is pass-through or planned until shared Ariada domain packages mature.
+
Flutter web evidence decision matrix
Decision point
Recommended S106 position
Why it matters
HTML-renderer or semantics-rich output
Treat as the best current target for the adapter because DOM-oriented evidence can observe meaningful labels, links, text, forms, headings, landmarks, legal notices and metadata.
This is the path where Ariada evidence can be useful immediately after a Flutter web build. It still needs a real Flutter SDK fixture in the next pass.
+
CanvasKit or Skwasm-heavy output
Mark as limited for DOM scanning and require native Flutter semantics tests, manual review, or future Ariada Flutter plugin work before compliance claims.
A canvas can be visually complete while exposing little ordinary HTML. The report must avoid overstating coverage.
+
Public marketing site built in Flutter web
Recommend Ariada only if the rendered output exposes text, metadata, links, language, legal notices and crawlable content.
Marketing and SEO/AIEO/GEO buyers care about discoverability and inspectable structure, not only visual parity with mobile.
+
Internal admin app deployed on web
Use Ariada as a release evidence packet for accessibility and legal-policy checks, but keep deeper workflow validation in Flutter widget and E2E tests.
Admin teams can accept CI evidence, but they still need keyboard, focus, modal, form and state-path tests outside a static scan.
+
Public-sector service surface
Require the strongest path: real build output, browser scan, screenshot, raw JSON, command log, manual reviewer sign-off and retained evidence.
EAA and EN 301 549 evidence is a procurement and acceptance artifact, not just a developer convenience.
+
Mobile-only Flutter app
Do not sell S106. Route to future mobile/app accessibility evidence work instead.
The distribution channel is Flutter web. Selling it to mobile-only teams would create wrong expectations.
+
FlutterFlow or generated Flutter web
Treat as adjacent future onboarding, not proof of this pub package. Generated web shells still need real screenshots and host-specific blockers documented.
No-code and generated-app teams may buy evidence, but packaging and support surfaces differ from pub.dev developers.
+
CI without local Dart
Prefer Docker/GitHub Action/hosted worker because the adapter source alone cannot prove package behavior without Dart SDK.
This mirrors the current host blocker and turns it into a product packaging requirement.
+
CI with Dart but no Flutter
Allow URL scanning of already served Flutter web output, but block claims about `flutter build web` integration.
Dart package tests can pass while the Flutter build path remains unproven.
+
CI with Flutter SDK
Run `flutter build web`, preserve `build/web`, run `dart run ariada:scan --static-dir build/web`, upload raw JSON, screenshots and HTML report.
This is the target happy path for the next S106 validation host.
+
Hosted scan
Hide Dart/Flutter/Node/browser setup and sell retention, signatures, baselines and dashboards.
Buyers pay to remove operational friction and keep evidence history.
+
Native Flutter plugin
Future path only. It should inspect Semantics, route coverage and widget-level accessibility before browser output exists.
A native plugin would be a different product surface from the current thin CLI wrapper.
+
Renderer-specific evidence adequacy
Renderer or output shape
Evidence classification
Adequacy statement
HTML-like DOM output
tested host surface can be meaningful
Ariada can inspect ordinary controls, labels, headings, language, links, legal notices, metadata, structured data and many cross-domain signals.
+
Flutter semantics DOM layer
partially meaningful host surface
Screen-reader-oriented structure may be present, but the report must still check whether labels, roles and focus semantics appear as expected.
+
CanvasKit canvas with minimal semantics
limited host surface
Ariada may see shell metadata and canvas element only. This is not enough for a compliance claim without native semantics tests or manual review.
+
Skwasm output
limited unless semantics are exposed
The WebAssembly renderer changes implementation details and may require separate capture/performance evidence.
+
Server shell plus Flutter app mount
mixed host surface
Ariada can inspect the shell, legal links, metadata and app mount, but may miss widget semantics if canvas-only.
+
Prerendered marketing shell with Flutter islands
promising surface
Ariada can inspect the public shell while separate checks handle Flutter islands. This may be the best SEO/AIEO/GEO route.
+
Single-page authenticated app
requires authenticated scan path
Future hosted worker or CI recipe must support auth/session setup before claims are useful.
+
Embedded Flutter web inside another host
host-specific evidence needed
The containing CMS, Angular, React or native shell can affect layout, accessibility, CSP and asset loading.
+
PWA installable Flutter web app
additional manifest and offline checks needed
Reliability, privacy, security and legal notice checks should include manifest, service worker, cache and update behavior.
+
Internationalized Flutter web app
locale-specific evidence needed
A single English fixture does not prove Swedish/EU language, labels, date formats, legal notices or RTL behavior.
+
Buyer objections and answers
Objection
Answer Ariada should give
Status today
Flutter already has accessibility APIs.
Yes, and Ariada should complement them by scanning the built web artifact and retaining external evidence. Native Flutter semantics checks are future work.
Documented.
+
CanvasKit is not normal HTML.
Correct. The report labels canvas-heavy output as limited and does not use a static DOM fixture to claim CanvasKit compliance.
Documented with caveat fixture.
+
Why install Node for a Dart package?
The wrapper is intentionally thin over the shared scanner. The next product step is a Docker/GitHub Action/hosted worker that hides Node and browser setup.
Open packaging gap.
+
Why not just use Lighthouse?
Lighthouse is useful, but Ariada is positioned as retained multi-domain compliance evidence with raw JSON, screenshots, command logs and future signed exports.
Positioned in competitor map.
+
Why pay for a wrapper?
Do not charge for the wrapper. Charge for retention, signatures, baselines, dashboards, exception workflows and compliance-domain packs.
Monetization section says this.
+
Can this prove EAA compliance?
No automated scanner alone proves compliance. It creates repeatable evidence and triage artifacts for human review.
Self-critique section says this.
+
Will it run in our CI?
Yes after Dart/Flutter/Node/browser setup exists. The current host lacks Dart/Flutter, so CI recipe is a required next artifact.
Host blocker documented.
+
What about authenticated routes?
Not implemented in S106. Future Action/hosted worker needs session setup and route inventory.
Future gap.
+
What about screenshots?
The evidence separates tested host surface from scan-result preview and avoids report-only proof.
Implemented.
+
What if pub.dev package name is unavailable?
Founder/release coordinator decides final name; source currently uses `ariada` to satisfy `dart run ariada:scan` in the spec.
Human blocker.
+
What about FlutterFlow users?
Adjacent channel. Use this research later, but do not claim FlutterFlow marketplace/product coverage in S106.
Scoped.
+
What about mobile accessibility?
Separate channel. S106 is web-output evidence and should not be sold as mobile app scanning.
Scoped.
+
pub.dev release readiness checklist
Release item
Why it matters
Current state
Package name decision
The command requested by the handoff is `dart run ariada:scan`, which implies package name `ariada`; pub.dev availability and brand fit must be confirmed.
Human blocker.
+
Verified publisher
Dart docs and pub.dev help emphasize publisher identity. Ariada should publish under a verified Ariada domain, not as an unverified uploader.
Human blocker.
+
License and repository metadata
pub.dev scoring and enterprise trust depend on clear license, repository and issue tracker metadata.
Present in `pubspec.yaml`; final publication still needs dry-run.
+
Executable mapping
Dart package layout expects public tools in `bin/`; the package exposes `scan` for `dart run ariada:scan`.
Implemented in source.
+
README install path
Dart users need exact commands and the shared CLI dependency explained up front.
Implemented.
+
Analyzer and format
Dart packages should pass `dart analyze` and `dart format --output=none --set-exit-if-changed .` before publication.
Blocked by missing Dart SDK.
+
Tests
Parser and runner tests should pass under `dart test` before publication.
Written, blocked by missing Dart SDK.
+
Publish dry-run
`dart pub publish --dry-run` catches metadata and package-shape issues before credentials are used.
Blocked by missing Dart SDK.
+
Flutter example
A real Flutter web sample build is stronger than a static fixture and should be included before public promotion.
Blocked by missing Flutter SDK.
+
CI recipe
The first public users should be able to copy a GitHub Action without manually composing Dart, Flutter, Node, browser and upload steps.
Not implemented.
+
Security disclosure
The package shells out to external CLI; docs should explain no secrets are collected and where artifacts are written.
Partially covered; needs release review.
+
Versioning
Start at 0.1.0 only after runtime gates pass. Keep pre-release/internal status until Dart/Flutter host validation is complete.
Returned Dart docs, pub.dev help, Stack Overflow package executable questions.
+
Search query
Research method
“Flutter web accessibility Semantics screen reader DOM”.
Returned official accessibility docs and community implementation questions.
+
Search query
Research method
“Flutter web CanvasKit SEO accessibility production readiness”.
Returned HN/Reddit/blog signals about public web fit.
+
Signal count
Pattern
Evidence cluster
Canvas versus DOM
Flutter docs, GitHub issues, Reddit, Stack Overflow and HN all surface the renderer split. S106 must classify evidence by rendered host surface, not by Flutter source alone.
+
Testing selectors and semantics friction
GitHub issue #97455, Stack Overflow Semantics questions, and Cypress/Playwright guides show that web testing can be awkward; Ariada should not hide setup.
+
Publishing trust
Dart docs, pub.dev help, verified publisher docs and Reddit publishing threads point to verified publisher/domain trust as a release blocker.
+
Performance and payload concerns
Renderer docs, community threads, Lighthouse competitors and sustainability sources all point to payload/performance as a future domain hook.
+
Compliance buyer absent from community threads
Most public signals are developers; buyer demand must be validated by interviews with platform/compliance owners.
+
Pain mining
Where to search next
Queries and signals to collect
Flutter GitHub issues
`is:issue web accessibility semantics CanvasKit`, `testID Flutter web`, `HTML renderer removed accessibility`; collect blocker labels and maintainer replies.
+
Reddit r/FlutterDev
`Flutter web accessibility`, `CanvasKit HTML renderer`, `pub.dev package publishing`; collect production anecdotes and objections.
+
Stack Overflow
`flutter web semantics`, `canvaskit accessibility`, `dart run executable package`; collect recurring setup questions.
+
HN/Lobsters
`Flutter web production ready accessibility SEO`; collect architect objections and language for positioning.
G2, Capterra, TrustRadius and Product Hunt: no strong Flutter-web-specific package buying signal found; treat as weak.
+
Distribution/monetization
Revenue layer
Decision
Free adapter
Keep pub.dev package free to seed Flutter web adoption and avoid charging for a thin wrapper.
+
Paid team dashboard
Charge for retained evidence, dashboards, baselines, waivers, SLA history, and multi-domain trend views.
+
Signed exports
Sell procurement-ready signed HTML/PDF/JSON evidence bundles for public-sector and enterprise acceptance.
+
Domain packs
Charge for privacy/GDPR, security, performance, sustainability, SEO/AIEO/GEO, legal notices, localization/i18n, data provenance, and AI/compliance packs as they mature.
+
Hosted worker
Hide Dart/Flutter/Node/browser setup in a hosted or CI runner so developers do not own brittle runtime plumbing.
+
Competitor comparison
Deque/Siteimprove/Evinced sell governance; BrowserStack/LambdaTest sell cloud testing; Ariada starts as open evidence adapter then monetizes retention and compliance workflow.
+
Sources incl community/review places where possible
Official Flutter/Dart/pub.dev/W3C/EU sources are used for stable mechanics, standards, and publishing rules. Community-review sources are used only for objections, adoption signals, and pain-mining language. Internal Ariada PRDs and package files are used for implementation boundaries and domain roadmap fit.
+
Self-critique and limitations
What this report does not prove
Next proof needed
It does not prove a real Flutter SDK build on this host.
Install Flutter and run `flutter build web --web-renderer html` against an example app.
+
It does not prove CanvasKit accessibility completeness.
Build a native Flutter semantics/testing connector or mark CanvasKit as limited for DOM scans.
+
It does not prove pub.dev name availability.
Release coordinator checks pub.dev and verified publisher setup.
+
It does not prove buyer willingness.
Interview platform owners, accessibility reviewers, and public-sector suppliers.
+
It does not prove all domains.
Implement/pass through shared Ariada domain packs as they mature.
+
Next steps for Ariada
Owner
Action
Adapter maintainer
Run Dart gates on a host with Dart SDK: `dart pub get`, `dart analyze`, `dart test`, `dart format --output=none --set-exit-if-changed .`.
+
Flutter maintainer
Create real sample app and run `flutter build web --web-renderer html`; preserve build output fixture.
+
Platform maintainer
Ship a GitHub Action/Docker recipe that hides Node/browser/Ariada CLI setup.
+
Product
Define paid retention, baseline policy, signed export, and exception workflow for Flutter web evidence.
+
Research
Run pain-mining queries monthly and update source/signal table.
+
Next steps for humans
Human role
Action
Founder/release coordinator
Approve pub.dev package name and verified publisher.
+
Compliance reviewer
Review whether fixture findings map to EAA/WCAG buyer language.
+
Flutter expert
Validate CanvasKit/Skwasm limitation and semantics-layer wording.
+
Sales/product
Test pricing language with platform owners and public-sector suppliers.
+
Human/agent handoff
Handoff item
Status
Changed files stay under `integrations/dart-flutter-ariada`
Yes.
+
Central hub files
Not edited by this work item per user instruction.
+
Mascot paths
Not staged.
+
Commit author
Alexander Brichkin (Agonist Development AB) .
+
Distribution/promotion
Surface
Message
pub.dev
Thin Ariada evidence adapter for Flutter web builds; scanner rules live in shared CLI.
+
GitHub README
Use after `flutter build web`; document renderer caveat and artifacts.
+
Flutter community
Ask for feedback on CI evidence and renderer limitations, not generic accessibility claims.
+
Public-sector procurement
Offer retained EAA/WCAG evidence, screenshots, raw JSON, command log, and signed exports.
+
What developers should not be asked to own
Flutter web teams should not own browser-runtime caching, Node-based scanner installation, evidence signing, retention, or cross-domain policy interpretation. The wrapper should make the first local run easy; CI/Docker/hosted paths should absorb operational friction.
Decision
Implication
Channel status
MVP bridge for Flutter web evidence, not a native Flutter scanner.
+
Scanner boundary
All scanning stays in shared Ariada CLI/core packages.
+
Host caveat
No Dart/Flutter SDK on this workstation; source and fixture are prepared, runtime gates documented as blocked.
+
Future native path
A truly native Flutter path would inspect Flutter semantics tests, widget trees, route maps, and generated web output together. S106 does not do that. The current channel is intentionally a thin evidence bridge around built web output and the shared Ariada CLI.
Decision
Implication
Channel status
MVP bridge for Flutter web evidence, not a native Flutter scanner.
+
Scanner boundary
All scanning stays in shared Ariada CLI/core packages.
+
Host caveat
No Dart/Flutter SDK on this workstation; source and fixture are prepared, runtime gates documented as blocked.
+
Buyer timing
Ariada should enter when a Flutter web app becomes public-facing, contractual, regulated, or procurement-reviewed. Pure mobile teams are not the buyer for this channel until they ship a web surface.
Decision
Implication
Channel status
MVP bridge for Flutter web evidence, not a native Flutter scanner.
+
Scanner boundary
All scanning stays in shared Ariada CLI/core packages.
+
Host caveat
No Dart/Flutter SDK on this workstation; source and fixture are prepared, runtime gates documented as blocked.
+
Report-only screenshot warning
A report-only screenshot is useful for presentation but cannot prove the tested host surface. This report embeds and links the tested-host PNG and separately captures the scan-result preview.
Decision
Implication
Channel status
MVP bridge for Flutter web evidence, not a native Flutter scanner.
+
Scanner boundary
All scanning stays in shared Ariada CLI/core packages.
+
Host caveat
No Dart/Flutter SDK on this workstation; source and fixture are prepared, runtime gates documented as blocked.
+
Host blocker exactness
The host blocker is concrete: `command -v dart` and `command -v flutter` return no executable in this worktree environment. That blocks Dart/package runtime gates and real Flutter build validation, but not static source review, fixture inspection, screenshot capture, or report audit.
Decision
Implication
Channel status
MVP bridge for Flutter web evidence, not a native Flutter scanner.
+
Scanner boundary
All scanning stays in shared Ariada CLI/core packages.
+
Host caveat
No Dart/Flutter SDK on this workstation; source and fixture are prepared, runtime gates documented as blocked.
+
Acceptance evidence still needed before public promotion
Evidence gap
Why it matters for S106
Concrete next proof
Real Flutter SDK build
A static fixture can prove the adapter and report path, but a public pub.dev announcement should show an actual Flutter project built with the documented renderer mode.
Create a tiny Flutter web app, run `flutter build web --web-renderer html` or the current supported equivalent, commit the generated representative fixture, and scan that output.
+
Renderer/version matrix
Flutter web renderer behavior changes across releases. A one-version result can become stale if HTML renderer support, CanvasKit semantics, or Skwasm defaults change.
Record Flutter version, Dart version, renderer/build mode, generated files, and screenshot classification in each evidence bundle.
+
Hosted CI proof
The product promise is stronger when Dart/Flutter/Node/browser setup is hidden from application developers.
Run the same fixture in a pinned Docker or GitHub Action environment and upload the full evidence bundle as an artifact.
+
Reviewer acceptance
The buyer is often an accessibility or compliance reviewer, not the developer who adds the package.
Ask reviewers whether raw JSON, command log, tested-host screenshot, scan-result preview and HTML report are sufficient for triage, and what signed export format they require.
+
Command log
$ dart run ariada:scan --static-dir fixtures/flutter-web-html-renderer/build/web --domains accessibility --severity-threshold moderate --output-dir scan-evidence/ariada-output --ariada-bin ariada-stub
+Host blocker: this workstation has no Dart or Flutter executable, so dart pub get, dart analyze, dart test, dart format, dart pub publish --dry-run, and a real flutter build web run could not execute locally.
+Adapter contract note: --static-dir serves the fixture on loopback and forwards --allow-private to the shared @ariada-org/cli; served local URLs require the explicit --allow-private wrapper flag.
+Validated instead: package source structure, fixture output path, synthetic shared-CLI JSON parser contract, screenshot capture, screenshot dimensions, nonblank pixels, and Dash-plus report audit.
+The stub evidence models the expected shared @ariada-org/cli output and demonstrates the adapter contract without reimplementing scanner rules.
+
+
Raw representative Ariada JSON
{
+ "sites": [
+ "file://fixtures/flutter-web-html-renderer/build/web/index.html"
+ ],
+ "domains": [
+ "accessibility"
+ ],
+ "grid": {
+ "file://fixtures/flutter-web-html-renderer/build/web/index.html": {
+ "accessibility": [
+ {
+ "ruleId": "ariada/images/alt-text",
+ "severity": "serious",
+ "message": "Fixture image has no alt text."
+ },
+ {
+ "ruleId": "ariada/forms/label",
+ "severity": "serious",
+ "message": "Email input has no associated label."
+ },
+ {
+ "ruleId": "ariada/buttons/name",
+ "severity": "moderate",
+ "message": "Button has no accessible name."
+ },
+ {
+ "ruleId": "ariada/statement/page-link-from-footer",
+ "severity": "moderate",
+ "message": "Accessibility statement link is missing from the footer."
+ }
+ ]
+ }
+ }
+}
+
+
+
+
\ No newline at end of file
diff --git a/integrations/dart-flutter-ariada/scan-evidence/scan-result-preview.html b/integrations/dart-flutter-ariada/scan-evidence/scan-result-preview.html
new file mode 100644
index 00000000..f7d1d950
--- /dev/null
+++ b/integrations/dart-flutter-ariada/scan-evidence/scan-result-preview.html
@@ -0,0 +1,37 @@
+
+Ariada Flutter web scan preview
+
Ariada Flutter web scan-result preview
Classification: scan-result preview. This is not the tested host surface; it renders the representative shared CLI JSON for screenshot capture.
+
Rule
Severity
Message
ariada/images/alt-text
serious
Fixture image has no alt text.
+
ariada/forms/label
serious
Email input has no associated label.
+
ariada/buttons/name
moderate
Button has no accessible name.
+
ariada/statement/page-link-from-footer
moderate
Accessibility statement link is missing from the footer.
Host blocker
$ dart run ariada:scan --static-dir fixtures/flutter-web-html-renderer/build/web --domains accessibility --severity-threshold moderate --output-dir scan-evidence/ariada-output --ariada-bin ariada-stub
+Host blocker: this workstation has no Dart or Flutter executable, so dart pub get, dart analyze, dart test, dart format, dart pub publish --dry-run, and a real flutter build web run could not execute locally.
+Adapter contract note: --static-dir serves the fixture on loopback and forwards --allow-private to the shared @ariada-org/cli; served local URLs require the explicit --allow-private wrapper flag.
+Validated instead: package source structure, fixture output path, synthetic shared-CLI JSON parser contract, screenshot capture, screenshot dimensions, nonblank pixels, and Dash-plus report audit.
+The stub evidence models the expected shared @ariada-org/cli output and demonstrates the adapter contract without reimplementing scanner rules.
+
`;
+const link = (label, href) => `${esc(label)}`;
+const linkTableRows = (items) => items.map(([label, owner, kind, href]) => row([esc(label), esc(owner), esc(kind), link(href, href)]));
+
+const localLinks = [
+ ['README', '../README.md'], ['pubspec.yaml', '../pubspec.yaml'], ['analysis options', '../analysis_options.yaml'],
+ ['Dart entrypoint', '../bin/scan.dart'], ['public library export', '../lib/ariada.dart'],
+ ['report parser', '../lib/src/report.dart'], ['runner wrapper', '../lib/src/runner.dart'],
+ ['parser tests', '../test/report_test.dart'], ['runner tests', '../test/runner_test.dart'],
+ ['HTML renderer fixture', '../fixtures/flutter-web-html-renderer/build/web/index.html'],
+ ['CanvasKit caveat fixture', '../fixtures/flutter-web-canvaskit/build/web/index.html'],
+ ['raw scan JSON', 'ariada-output/multi-domain-report.json'], ['command log', 'command.log'],
+ ['command exit', 'command.exit'], ['tested host screenshot', 'screenshots/tested-host-surface.png'],
+ ['scan result screenshot', 'screenshots/scan-result.png'], ['scan preview', 'scan-result-preview.html'],
+ ['test report', '../test-report/result.html'],
+];
+
+const externalSources = [
+ ['Flutter web renderers', 'Flutter docs', 'official primary', 'https://docs.flutter.dev/platform-integration/web/renderers'],
+ ['Flutter web accessibility', 'Flutter docs', 'official primary', 'https://docs.flutter.dev/ui/accessibility/web-accessibility'],
+ ['Flutter accessibility overview', 'Flutter docs', 'official primary', 'https://docs.flutter.dev/ui/accessibility'],
+ ['Flutter accessibility testing', 'Flutter docs', 'official primary', 'https://docs.flutter.dev/testing/accessibility'],
+ ['Dart package layout', 'Dart docs', 'official primary', 'https://dart.dev/tools/pub/package-layout'],
+ ['Dart publishing packages', 'Dart docs', 'official primary', 'https://dart.dev/tools/pub/publishing'],
+ ['pub.dev publishing help', 'pub.dev', 'official primary', 'https://pub.dev/help/publishing'],
+ ['Dart verified publishers', 'Dart docs', 'official primary', 'https://dart.dev/tools/pub/verified-publishers'],
+ ['Dart pub global', 'Dart docs', 'official primary', 'https://dart.dev/tools/pub/cmd/pub-global'],
+ ['Dart testing', 'Dart docs', 'official primary', 'https://dart.dev/tools/testing'],
+ ['dart test command', 'Dart docs', 'official primary', 'https://dart.dev/tools/dart-test'],
+ ['Dart analysis options', 'Dart docs', 'official primary', 'https://dart.dev/tools/analysis'],
+ ['package:test', 'pub.dev', 'registry primary', 'https://pub.dev/packages/test'],
+ ['package:lints', 'pub.dev', 'registry primary', 'https://pub.dev/packages/lints'],
+ ['package:flutter_lints', 'pub.dev', 'registry primary', 'https://pub.dev/packages/flutter_lints'],
+ ['Flutter web renderer removal issue', 'Flutter GitHub', 'community/project issue', 'https://github.com/flutter/flutter/issues/145954'],
+ ['Flutter web classes/testID issue', 'Flutter GitHub', 'community/project issue', 'https://github.com/flutter/flutter/issues/97455'],
+ ['Flutter CanvasKit offline issue', 'Flutter GitHub', 'community/project issue', 'https://github.com/flutter/flutter/issues/85624'],
+ ['Flutter CanvasKit iOS issue', 'Flutter GitHub', 'community/project issue', 'https://github.com/flutter/flutter/issues/91414'],
+ ['CanvasKit mobile stretch issue', 'Flutter GitHub', 'community/project issue', 'https://github.com/flutter/flutter/issues/159974'],
+ ['HTML renderer announcement', 'flutter-announce', 'official/community', 'https://groups.google.com/g/flutter-announce/c/JqkMe7cPkQo'],
+ ['Flutter web accessibility article', 'Flutter blog', 'official secondary', 'https://blog.flutter.dev/accessibility-in-flutter-on-the-web-51bfc558b7d3'],
+ ['Flutter web renderer Reddit 1', 'Reddit r/FlutterDev', 'community discussion', 'https://www.reddit.com/r/FlutterDev/comments/10ix09l/flutter_web_canvaskit_or_html_renderer/'],
+ ['Flutter web renderer Reddit 2', 'Reddit r/FlutterDev', 'community discussion', 'https://www.reddit.com/r/FlutterDev/comments/1329g4g/do_you_use_flutter_web_do_you_explicitly_set/'],
+ ['Flutter web milestones Reddit', 'Reddit r/FlutterDev', 'community discussion', 'https://www.reddit.com/r/FlutterDev/comments/1c9x03h/what_is_the_major_milestones_that_flutter_web/'],
+ ['Publishing Flutter package Reddit', 'Reddit r/FlutterDev', 'community discussion', 'https://www.reddit.com/r/FlutterDev/comments/1p0edm7/i_wrote_a_stepbystep_guide_on_how_to_publish_a/'],
+ ['CanvasKit Stack Overflow tag', 'Stack Overflow', 'community Q&A', 'https://stackoverflow.com/questions/tagged/canvaskit'],
+ ['Flutter web accessibility Semantics', 'Stack Overflow', 'community Q&A', 'https://stackoverflow.com/questions/67931553/using-semantics-widget-in-flutter-web'],
+ ['CanvasKit folder question', 'Stack Overflow', 'community Q&A', 'https://stackoverflow.com/questions/71221004/is-folder-canvaskit-part-of-the-output-of-the-flutter-web'],
+ ['Flutter web CanvasKit on iOS', 'Stack Overflow', 'community Q&A', 'https://stackoverflow.com/questions/69073328/flutter-web-with-canvaskit-on-ios-15-beta'],
+ ['How to use CanvasKit', 'Stack Overflow', 'community Q&A', 'https://stackoverflow.com/questions/64583461/how-to-use-skia-canvaskit-in-flutter-web'],
+ ['HN Flutter web discussion', 'Hacker News', 'community discussion', 'https://news.ycombinator.com/item?id=26333239'],
+ ['Flutter Cypress guide', 'Autonoma', 'community/vendor article', 'https://getautonoma.com/blog/flutter-cypress-testing-guide'],
+ ['Practical Flutter accessibility', 'DCM', 'community/vendor article', 'https://dcm.dev/blog/2025/06/30/accessibility-flutter-practical-tips-tools-code-youll-actually-use/'],
+ ['Flutter static analysis guide', 'DCM', 'community/vendor article', 'https://dcm.dev/blog/2025/10/21/getting-started-flutter-static-analytics-lints/'],
+ ['FlutterFlow accessibility docs', 'FlutterFlow', 'vendor docs', 'https://docs.flutterflow.io/concepts/accessibility/'],
+ ['Very Good Ventures accessibility', 'Very Good Ventures', 'community/vendor article', 'https://verygood.ventures/blog/exploring-accessibility-and-digital-inclusion-with-flutter/'],
+ ['Pub package executable Q&A', 'Stack Overflow', 'community Q&A', 'https://stackoverflow.com/questions/77553247/how-to-create-a-executable-script-on-my-flutter-package'],
+ ['Pub documentation after publishing', 'Stack Overflow', 'community Q&A', 'https://stackoverflow.com/questions/74910555/can-i-edit-package-documentation-on-pub-dev-after-publishing'],
+ ['Dart unit test Q&A', 'Stack Overflow', 'community Q&A', 'https://stackoverflow.com/questions/59812714/running-all-unit-tests-in-dart'],
+ ['Dart test GitHub', 'GitHub', 'project source', 'https://github.com/dart-lang/test'],
+ ['Dart pub binary issue', 'GitHub dart-lang/pub', 'project issue', 'https://github.com/dart-lang/pub/issues/407'],
+ ['dart-lang ecosystem lints', 'GitHub', 'project source', 'https://github.com/dart-lang/ecosystem/blob/main/pkgs/dart_flutter_team_lints/lib/analysis_options.yaml'],
+ ['pub.dev homepage', 'pub.dev', 'registry primary', 'https://pub.dev/'],
+ ['axe platform', 'Deque', 'vendor primary', 'https://www.deque.com/axe/'],
+ ['axe-core repository', 'GitHub', 'vendor source', 'https://github.com/dequelabs/axe-core'],
+ ['axe DevTools CLI', 'Deque docs', 'vendor primary', 'https://docs.deque.com/devtools-for-web/4/en/cli-home/'],
+ ['axe rules', 'Deque University', 'vendor primary', 'https://dequeuniversity.com/rules/axe/html'],
+ ['@axe-core/cli', 'npm', 'registry primary', 'https://www.npmjs.com/package/@axe-core/cli'],
+ ['Pa11y home', 'Pa11y', 'project primary', 'https://pa11y.org/'],
+ ['Pa11y repository', 'GitHub', 'project source', 'https://github.com/pa11y/pa11y'],
+ ['Pa11y CI', 'GitHub', 'project source', 'https://github.com/pa11y/pa11y-ci'],
+ ['Lighthouse CI', 'GitHub', 'project source', 'https://github.com/GoogleChrome/lighthouse-ci'],
+ ['Lighthouse accessibility', 'Chrome docs', 'vendor primary', 'https://developer.chrome.com/docs/lighthouse/accessibility/'],
+ ['WAVE', 'WebAIM', 'vendor primary', 'https://wave.webaim.org/'],
+ ['BrowserStack accessibility testing', 'BrowserStack', 'vendor primary', 'https://www.browserstack.com/accessibility-testing'],
+ ['LambdaTest accessibility testing', 'LambdaTest', 'vendor primary', 'https://www.lambdatest.com/accessibility-testing'],
+ ['Siteimprove accessibility', 'Siteimprove', 'vendor primary', 'https://www.siteimprove.com/solutions/accessibility/'],
+ ['AudioEye', 'AudioEye', 'vendor primary', 'https://www.audioeye.com/'],
+ ['Evinced', 'Evinced', 'vendor primary', 'https://www.evinced.com/'],
+ ['Level Access', 'Level Access', 'vendor primary', 'https://www.levelaccess.com/'],
+ ['Equalize Digital checker', 'Equalize Digital', 'vendor primary', 'https://equalizedigital.com/accessibility-checker/'],
+ ['OWASP ZAP', 'OWASP', 'project primary', 'https://www.zaproxy.org/'],
+ ['SecurityHeaders', 'SecurityHeaders', 'tool primary', 'https://securityheaders.com/'],
+ ['Mozilla Observatory', 'Mozilla', 'tool primary', 'https://observatory.mozilla.org/'],
+ ['Cookiebot', 'Usercentrics', 'vendor primary', 'https://www.cookiebot.com/'],
+ ['OneTrust', 'OneTrust', 'vendor primary', 'https://www.onetrust.com/'],
+ ['Website Carbon', 'Wholegrain Digital', 'tool primary', 'https://www.websitecarbon.com/'],
+ ['Ecograder', 'Mightybytes', 'tool primary', 'https://ecograder.com/'],
+ ['Google Rich Results Test', 'Google Search Central', 'vendor primary', 'https://search.google.com/test/rich-results'],
+ ['Schema.org validator', 'Schema.org', 'tool primary', 'https://validator.schema.org/'],
+ ['W3C Nu Checker', 'W3C', 'primary standards tool', 'https://validator.w3.org/nu/'],
+ ['W3C WAI testing overview', 'W3C WAI', 'standards guidance', 'https://www.w3.org/WAI/test-evaluate/'],
+ ['WCAG 2.2', 'W3C', 'standard primary', 'https://www.w3.org/TR/WCAG22/'],
+ ['EN 301 549', 'ETSI', 'standard primary', 'https://www.etsi.org/deliver/etsi_en/301500_301599/301549/'],
+ ['European Accessibility Act', 'European Commission', 'regulatory primary', 'https://commission.europa.eu/strategy-and-policy/policies/justice-and-fundamental-rights/disability/european-accessibility-act-eaa_en'],
+ ['AccessibleEU EAA timing', 'AccessibleEU', 'official secondary', 'https://accessible-eu-centre.ec.europa.eu/content-corner/news/eaa-comes-effect-june-2025-are-you-ready-2025-01-31_en'],
+ ['GDPR text', 'EUR-Lex', 'law primary', 'https://eur-lex.europa.eu/eli/reg/2016/679/oj/eng'],
+ ['EU AI Act Article 50', 'EU AI Act Service Desk', 'official guidance', 'https://ai-act-service-desk.ec.europa.eu/en/ai-act/article-50'],
+ ['W3C Web Sustainability Guidelines', 'W3C', 'draft standard', 'https://www.w3.org/TR/web-sustainability-guidelines/'],
+ ['Web Vitals', 'web.dev', 'vendor guidance', 'https://web.dev/articles/vitals'],
+ ['Core Web Vitals and Search', 'Google Search Central', 'vendor guidance', 'https://developers.google.com/search/docs/appearance/core-web-vitals'],
+ ['GitHub Actions artifacts', 'GitHub Docs', 'vendor primary', 'https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts'],
+ ['GitLab job artifacts', 'GitLab Docs', 'vendor primary', 'https://docs.gitlab.com/ci/jobs/job_artifacts/'],
+ ['OpenSSF Scorecard', 'OpenSSF', 'project primary', 'https://securityscorecards.dev/'],
+ ['SLSA framework', 'SLSA', 'project primary', 'https://slsa.dev/'],
+ ['Sigstore', 'Sigstore', 'project primary', 'https://www.sigstore.dev/'],
+];
+
+const roles = [
+ ['Flutter web developer', 'Runs one Dart-shaped command after `flutter build web`, without learning scanner internals.', 'Usually not payer; creates the adoption pull by proving fixture and CI usefulness.', 'Local release check or CI job for public Flutter web bundles.', 'MVP bridge implemented; real Flutter SDK run is host-blocked here.'],
+ ['Mobile-first Flutter team lead', 'Learns when web accessibility evidence is feasible and when CanvasKit output limits DOM scanning.', 'Influences platform budget; buys only when web becomes customer-facing.', 'Before converting an internal Flutter app into a public web surface.', 'Caveat documented; native Flutter semantics checks are not implemented here.'],
+ ['Platform or CI owner', 'Gets repeatable JSON/log/screenshot/HTML artifacts with standard exit codes.', 'Pays from platform/tooling budget once evidence becomes a release gate.', 'Pre-merge, nightly, or release artifact generation.', 'Wrapper contract exists; reusable GitHub Action/Docker path remains next work.'],
+ ['Accessibility reviewer', 'Receives direct fixture screenshot, raw JSON, command log, and report instead of a screenshot-only claim.', 'Influences compliance purchase and remediation priority.', 'Review of EAA/WCAG evidence for a Flutter web release.', 'Evidence path implemented against representative fixture; real app proof pending Flutter SDK.'],
+ ['Security or compliance owner', 'Can later combine accessibility, privacy/GDPR, security, performance, sustainability and AI/compliance evidence.', 'Enterprise compliance/legal budget when retained evidence is required.', 'Procurement, regulated release, or supplier acceptance.', 'Accessibility evidence exists; multi-domain hosted retention is not implemented.'],
+ ['Public-sector supplier', 'Needs EN 301 549/EAA evidence when a Flutter web bundle is delivered as a public service.', 'Project or contract budget.', 'Before acceptance testing or remedial sign-off.', 'Local evidence bundle is available; signed exports and audit retention are future paid layer.'],
+];
+
+const cultureRows = [
+ ['Fast local/dev loop', 'Dart teams accept `dart format`, `dart analyze`, `dart test`, `flutter test`, package lints, and small explicit `dart run` tools. Ariada belongs as an explicit scan command, not hidden inside unit tests.'],
+ ['CI/release loop', 'Browser-driven DOM scans, Node-based shared CLI setup, screenshot capture, and retained artifacts are acceptable in CI, nightly, release, and compliance workflows.'],
+ ['Rejected pattern', 'A surprise browser/Node scan inside every Flutter widget test would feel foreign and slow. CanvasKit output also makes conventional DOM checks incomplete.'],
+ ['Packaging expectation', 'pub.dev package, `bin/scan.dart`, `executables: scan`, `dart run ariada:scan`, optional `dart pub global activate`, and copy-paste CI examples.'],
+ ['Foreign dependency', 'The scanner remains the shared `@ariada-org/cli`, currently distributed via Node/npm. This Dart package is not a native scanner and should not claim to be one.'],
+ ['Workflow placement', 'Best placement is after `flutter build web`, in pre-merge CI, nightly evidence, release gates, procurement packets, and hosted fleet scans.'],
+];
+
+const solutionRows = [
+ ['Primary entrypoint', '`dart run ariada:scan --static-dir build/web` after a Flutter web build. It is explicit, pub.dev-shaped, and easy to add to CI.'],
+ ['Fallback entrypoint', 'Reusable GitHub Action or Docker image that installs Dart/Flutter, shared Ariada CLI, browser runtime, and uploads artifacts.'],
+ ['Free/open-source layer', 'Thin Dart wrapper, fixture, parser tests, artifact convention, and report generator.'],
+ ['Paid/hosted layer', 'Retained evidence, signed exports, baseline policies, exception workflow, team dashboards, and domain packs.'],
+ ['What developers should not own', 'Do not make each Flutter team hand-assemble Playwright caches, npm global installs, screenshot validation, evidence signing, or archival retention.'],
+ ['Next idiomatic version', 'A verified pub.dev package, Action/Docker recipe, Flutter SDK example app, HTML-renderer guidance, and hosted upload path.'],
+];
+
+const implementedRows = [
+ ['pub package skeleton', badge('ok', 'IMPLEMENTED'), '`pubspec.yaml` defines a Dart package, executable `scan`, metadata, lints, args/path dependencies, and test dependency.'],
+ ['Dart entrypoint', badge('ok', 'IMPLEMENTED'), '`bin/scan.dart` parses `--url`, `--static-dir`, `--domains`, `--severity-threshold`, `--output-dir`, and `--ariada-bin`.'],
+ ['Shared CLI invocation', badge('ok', 'IMPLEMENTED'), '`Process.run` invokes `@ariada-org/cli` via `ariada` or `ARIADA_BIN`. No scanner rule is implemented in Dart.'],
+ ['JSON parser', badge('ok', 'IMPLEMENTED'), '`MultiDomainReport` reads Ariada `grid` output and counts findings at or above the configured threshold.'],
+ ['Static-dir bridge', badge('ok', 'IMPLEMENTED'), 'The wrapper can serve `build/web` on loopback and pass the generated URL to the shared CLI.'],
+ ['Representative HTML fixture', badge('ok', 'IMPLEMENTED'), 'Fixture models a DOM-scannable Flutter web HTML-renderer output with known defects.'],
+ ['CanvasKit caveat fixture', badge('ok', 'IMPLEMENTED'), 'Second fixture documents a canvas-heavy output shape where DOM scanners have limited signal.'],
+ ['Unit tests', badge('warn', 'WRITTEN, HOST-BLOCKED'), '`test/report_test.dart` and `test/runner_test.dart` cover parsing and stub CLI contract, but Dart is not installed here.'],
+ ['Dart analyze/format/pub dry-run', badge('warn', 'HOST BLOCKER'), 'Blocked because neither `dart` nor `flutter` exists on this workstation path.'],
+ ['Real Flutter build', badge('warn', 'HOST BLOCKER'), 'Blocked by missing Flutter SDK and renderer-specific build support. The fixture proves evidence shape, not full Flutter runtime coverage.'],
+ ['pub.dev publication', badge('warn', 'HUMAN BLOCKER'), 'Requires Google account, verified publisher, final package-name decision, and release credentials.'],
+ ['Hosted retention and signing', badge('info', 'NOT IMPLEMENTED'), 'Local artifacts are generated; paid signed exports and retention belong to hosted Ariada.'],
+ ['Scanner rules', badge('info', 'NOT IMPLEMENTED HERE'), 'Accessibility, security, privacy/GDPR, performance, reliability, sustainability, SEO/AIEO/GEO, legal notices, localization/i18n, data provenance, and AI/compliance rules remain in shared Ariada domains.'],
+];
+
+const domainRows = [
+ ['Accessibility', 'implemented for fixture', 'Current evidence uses missing image alt, missing input label, unnamed button, and missing accessibility-statement link. Flutter semantics can expose accessible DOM, but CanvasKit/Skwasm shapes require caution.'],
+ ['Security', 'planned', 'Flutter web releases still need CSP, security headers, dependency and third-party script posture. Ariada should pass the shared security domain through, not implement it in Dart.'],
+ ['Privacy/GDPR', 'planned', 'Cookie notices, analytics tags, consent links, and data minimization claims belong in shared privacy checks. Flutter teams often embed analytics SDKs at the web shell.'],
+ ['Performance', 'planned', 'Flutter web payload size, CanvasKit assets, WebAssembly, and initial render timing are key buying hooks. Use shared performance domain when D07 matures.'],
+ ['Reliability', 'planned', 'Release evidence should include route availability, blank-screen risk, asset load failure, and broken link checks for web bundles.'],
+ ['Sustainability', 'planned', 'Flutter web can ship large binary/runtime assets. Domain should measure transfer size and third-party cost via shared Ariada logic.'],
+ ['SEO/AIEO/GEO', 'planned', 'Canvas-heavy output can be weak for public search and AI citation. HTML shell metadata, structured content, and crawlability must be tested separately.'],
+ ['Legal notices', 'planned', 'Footer links for accessibility statement, privacy policy, imprint/legal notice, and terms are important for EU public websites and procurement review.'],
+ ['Localization/i18n', 'planned', 'Flutter apps need `lang`, translated labels, locale-specific legal notices, and bidirectional text checks.'],
+ ['Data provenance', 'planned', 'Useful when Flutter web surfaces dashboards, datasets, or generated content that need source lineage.'],
+ ['AI/compliance', 'planned', 'Future checks can verify AI disclosure, EU AI Act notices, and generated-content transparency where relevant.'],
+ ['Native Flutter semantics', 'blocked', 'Ariada does not inspect Flutter widget trees or semantics tests today; this would require a separate Flutter-native plugin path.'],
+];
+
+const competitors = [
+ ['axe / axe DevTools CLI', 'Strong browser accessibility engine and commercial reporting.', 'Ariada should not claim stronger raw accessibility maturity; the wedge is Flutter-web evidence packaging plus multi-domain roadmap.'],
+ ['Pa11y / Pa11y CI', 'OSS command-line accessibility scans for web pages.', 'Ariada differentiates by retaining screenshot/log/raw JSON/report bundles and expanding beyond accessibility.'],
+ ['Lighthouse CI', 'Performance/accessibility/SEO scan in CI.', 'Flutter teams may already accept it in release workflows. Ariada should complement with EAA-oriented evidence and domain-specific policy.'],
+ ['Flutter built-in semantics and tests', 'Native widget/semantics checks before rendering to web.', 'Ariada complements them by scanning the built surface and producing external evidence.'],
+ ['Cypress/Playwright visual and E2E tests', 'Common for web interaction proof, including Flutter web workarounds.', 'Ariada can reuse their CI placement but focuses on compliance evidence.'],
+ ['Deque/Siteimprove/Evinced/Level Access', 'Enterprise accessibility governance.', 'Ariada is lighter and channel-specific now; paid hosted retention is the enterprise path.'],
+ ['BrowserStack/LambdaTest accessibility', 'Cloud testing and accessibility products.', 'Ariada should compete on open adapter plus evidence retention, not broad device cloud coverage.'],
+ ['SecurityHeaders/Observatory/ZAP', 'Security posture tools.', 'They are adjacent; Ariada security domain should aggregate release evidence, not replace specialist testing.'],
+ ['Cookiebot/OneTrust', 'Consent/privacy management.', 'Ariada can detect and retain evidence; it does not replace consent operations.'],
+ ['Website Carbon/Ecograder', 'Sustainability scoring.', 'Ariada can bring sustainability into the same release packet as accessibility and legal evidence.'],
+ ['Google Rich Results/Schema validator', 'Structured-data and SEO validators.', 'Ariada can retain and compare results for Flutter shells and public pages.'],
+ ['Manual audit consultancies', 'Human review and remediation.', 'Ariada does not replace humans; it sells repeatable evidence and triage packets.'],
+];
+
+const communitySignals = [
+ ['Flutter GitHub issues', 'Developer/maintainer', 'Renderer transitions, test selectors, CanvasKit behavior, and accessibility surface limitations appear repeatedly.', 'Strong: product must label S106 as MVP bridge and avoid native-scanner claims.'],
+ ['Reddit r/FlutterDev', 'Developer/team lead', 'Renderer choice, production readiness, and accessibility concerns recur in peer discussion.', 'Medium: useful pain language, not market proof.'],
+ ['Stack Overflow', 'Developer', 'CanvasKit deployment, Semantics widget confusion, executable package questions, and test running questions appear as implementation pain.', 'Medium: confirms docs and examples must be explicit.'],
+ ['Hacker News', 'Developer/architect', 'Flutter web debates emphasize web-native expectations, DOM/canvas tradeoffs, and production skepticism.', 'Weak-to-medium: broad sentiment, useful for positioning.'],
+ ['pub.dev and Dart docs', 'Maintainer/release owner', 'Verified publisher, package layout, executable scripts, lints, tests, and publishing are the idiomatic distribution path.', 'Strong for packaging, not community pain.'],
+ ['FlutterFlow docs/community', 'No-code/platform owner', 'Accessibility surfaces also matter for Flutter-derived web tools.', 'Weak: adjacent channel, useful for future hosted scan onboarding.'],
+ ['Vendor blogs and guides', 'Consultant/developer advocate', 'Flutter web testing and accessibility guides emphasize semantics, selectors, and CI setup.', 'Medium: helps write onboarding copy.'],
+ ['No-signal searches', 'Buyer/compliance owner', 'G2/Capterra/Product Hunt did not provide channel-specific Flutter web accessibility package buying evidence.', 'Documented weak signal; prefer GitHub/Reddit/Stack Overflow.'],
+ ['Search query', 'Research method', '“Flutter web accessibility CanvasKit HTML renderer semantics GitHub issue”.', 'Returned official docs, GitHub issues, Reddit, Stack Overflow, and vendor guides.'],
+ ['Search query', 'Research method', '“pub.dev publishing verified publisher Dart executable package”.', 'Returned Dart docs, pub.dev help, Stack Overflow package executable questions.'],
+ ['Search query', 'Research method', '“Flutter web accessibility Semantics screen reader DOM”.', 'Returned official accessibility docs and community implementation questions.'],
+ ['Search query', 'Research method', '“Flutter web CanvasKit SEO accessibility production readiness”.', 'Returned HN/Reddit/blog signals about public web fit.'],
+];
+
+const repeatedPatterns = [
+ ['Canvas versus DOM', 'Flutter docs, GitHub issues, Reddit, Stack Overflow and HN all surface the renderer split. S106 must classify evidence by rendered host surface, not by Flutter source alone.'],
+ ['Testing selectors and semantics friction', 'GitHub issue #97455, Stack Overflow Semantics questions, and Cypress/Playwright guides show that web testing can be awkward; Ariada should not hide setup.'],
+ ['Publishing trust', 'Dart docs, pub.dev help, verified publisher docs and Reddit publishing threads point to verified publisher/domain trust as a release blocker.'],
+ ['Performance and payload concerns', 'Renderer docs, community threads, Lighthouse competitors and sustainability sources all point to payload/performance as a future domain hook.'],
+ ['Compliance buyer absent from community threads', 'Most public signals are developers; buyer demand must be validated by interviews with platform/compliance owners.'],
+];
+
+const monetizationRows = [
+ ['Free adapter', 'Keep pub.dev package free to seed Flutter web adoption and avoid charging for a thin wrapper.'],
+ ['Paid team dashboard', 'Charge for retained evidence, dashboards, baselines, waivers, SLA history, and multi-domain trend views.'],
+ ['Signed exports', 'Sell procurement-ready signed HTML/PDF/JSON evidence bundles for public-sector and enterprise acceptance.'],
+ ['Domain packs', 'Charge for privacy/GDPR, security, performance, sustainability, SEO/AIEO/GEO, legal notices, localization/i18n, data provenance, and AI/compliance packs as they mature.'],
+ ['Hosted worker', 'Hide Dart/Flutter/Node/browser setup in a hosted or CI runner so developers do not own brittle runtime plumbing.'],
+ ['Competitor comparison', 'Deque/Siteimprove/Evinced sell governance; BrowserStack/LambdaTest sell cloud testing; Ariada starts as open evidence adapter then monetizes retention and compliance workflow.'],
+];
+
+function sourceParagraphs() {
+ return `
+
What is Flutter web? Flutter web is the browser-targeted deployment mode for Flutter applications. For Ariada it is not simply “another Dart app”: the scannable surface is the generated web output after Flutter rendering choices, semantics, assets, and shell HTML have been applied.
+
Why this is a separate Ariada channel: pub.dev is the normal package distribution route for Dart and Flutter developers, and dart run package:executable is a recognizable way to add explicit tooling. Flutter web also has a unique accessibility problem: some output can expose a semantics-backed DOM, while canvas-heavy rendering can make conventional DOM scanners less complete.
+
Source reliability: official Dart, Flutter, W3C, EU, pub.dev and standards pages are high-reliability primary or standards sources. GitHub issues, Reddit, Stack Overflow and Hacker News are community-review evidence only: they identify pain language and repeated objections, not market-size proof or legal truth.
${table(['Decision', 'Implication'], [
+ row(['Channel status', 'MVP bridge for Flutter web evidence, not a native Flutter scanner.']),
+ row(['Scanner boundary', 'All scanning stays in shared Ariada CLI/core packages.']),
+ row(['Host caveat', 'No Dart/Flutter SDK on this workstation; source and fixture are prepared, runtime gates documented as blocked.']),
+ ])}`);
+}
+
+const sections = [
+ section('What is Flutter web?', sourceParagraphs()),
+ section('Why this is a separate Ariada channel', `
Flutter web deserves its own channel because Dart/Flutter teams discover tools through pub.dev and expect Dart-shaped commands. The audience is smaller than raw Flutter adoption: only Flutter web outputs with useful web semantics can be meaningfully scanned by a DOM-oriented Ariada run.
${table(['Reason', 'S106 answer'], [
+ row(['pub.dev distribution', 'Package exposes dart run ariada:scan, matching the S106 handoff contract.']),
+ row(['Flutter renderer split', 'Report explicitly distinguishes HTML/semantic output from CanvasKit/Skwasm-heavy output.']),
+ row(['Compliance evidence', 'Ariada artifacts attach to built web output, not mobile widget source.']),
+ ])}`),
+ section('Channel culture fit', table(['Expectation', 'S106 fit'], cultureRows.map(([a, b]) => row([esc(a), esc(b)])))),
+ section('Recommended product solution', table(['Layer', 'Decision'], solutionRows.map(([a, b]) => row([esc(a), esc(b)])))),
+ section('Roles: who pays / what value they buy', `
The exact mandated table follows. It maps hooks, payer, buying moment, and implementation state.
`),
+ section('Кому что продаем: роли, hooks, кто платит и что уже готово', table(['Role', 'Hook', 'Who pays', 'Buying moment', 'Implemented state'], roles.map((r) => row(r.map(esc))))),
+ section('Implemented vs not implemented', table(['Capability', 'Status', 'Evidence'], implementedRows.map(([a, b, c]) => row([esc(a), b, c])))),
+ section('Ariada core used', `
The Dart code invokes the shared CLI and parses the shared report format. It does not implement WCAG checks, browser automation, privacy detection, security checks, performance scoring, sustainability scoring, SEO/AIEO/GEO analysis, legal notice checks, localization checks, data-provenance checks, or AI/compliance checks.
The tested host surface screenshot is the representative Flutter web/static output fixture. The scan-result preview screenshot is a rendered evidence summary. A report-only screenshot would be supplemental only; it is not used as the sole visual evidence.
${table(['Screenshot', 'Classification', 'Adequacy'], [
+ row([link('tested-host-surface.png', 'screenshots/tested-host-surface.png'), 'tested host surface', 'Primary visual evidence; shows the HTML fixture that the scan evidence represents.']),
+ row([link('scan-result.png', 'screenshots/scan-result.png'), 'scan-result preview', 'Secondary evidence; shows parsed findings from Ariada JSON.']),
+ row(['result.html screenshot evidence', 'linked PNG plus host blocker', 'Not counted alone; links the tested-host screenshot and documents the host blocker.']),
+ ])}`),
+ section('Visual evidence review', `
Visual evidence classification: tested-host-surface, scan-result preview, and report links are intentionally separated. This avoids the VISUAL_EVIDENCE_GAP where a report only screenshots itself. The fixture screenshot is expected to show a white app panel, green chips, an image placeholder, unlabeled email input, unnamed button, and missing statement link text. The scan preview is expected to show four findings and the host-blocker note.
${table(['Review item', 'Result'], [
+ row(['Standalone PNG link', link('screenshots/tested-host-surface.png', 'screenshots/tested-host-surface.png')]),
+ row(['Tested-host screenshot', hasTestedHostPng ? 'Captured and linked from screenshots/tested-host-surface.png.' : 'Pending screenshot capture.']),
+ row(['Nonblank validation', 'Validated by scripts/validate-screenshots.mjs after capture.']),
+ ])}`),
+ section('Evidence artifacts', table(['Artifact', 'Purpose'], [
+ row([link('multi-domain-report.json', 'ariada-output/multi-domain-report.json'), 'Representative shared CLI JSON output consumed by the wrapper.']),
+ row([link('command.log', 'command.log'), 'Documents exact attempted command and Dart/Flutter host blocker.']),
+ row([link('command.exit', 'command.exit'), 'Records host blocker outcome.']),
+ row([link('scan-result-preview.html', 'scan-result-preview.html'), 'Rendered scan-result preview used for screenshot capture.']),
+ row([link('result.html', 'result.html'), 'Dash-style full research report.']),
+ ])),
+ section('Evidence/test cases', table(['Case', 'Expected signal'], [
+ row(['Parser fixture', 'Two serious findings and two moderate findings are counted at moderate threshold.']),
+ row(['Runner stub test', 'Stub shared CLI writes multi-domain-report.json; wrapper returns exit 1.']),
+ row(['Static-dir fixture', 'Loopback server bridges build/web style output to the shared CLI.']),
+ row(['CanvasKit caveat fixture', 'Documents low-DOM output as a limitation rather than pretending coverage.']),
+ row(['Screenshot validation', 'Dimensions and nonblank pixels checked locally.']),
+ ])),
+ section('Verification and test adequacy', `
Test adequacy is partial because the host lacks Dart and Flutter. The source includes Dart tests and package metadata, but local execution could not prove analyzer, format, pub resolution, or dart test. The evidence still validates the fixture path, report path, screenshot path, and report completeness.
${table(['Gate', 'Status'], [
+ row(['command -v dart', 'not found']),
+ row(['command -v flutter', 'not found']),
+ row(['node scripts/validate-screenshots.mjs', 'locally runnable and required before commit']),
+ row(['Dash-plus audit', 'locally runnable with root audit script and Dash baseline']),
+ ])}`),
+ section('Blockers', table(['Blocker', 'Exact owner/action'], [
+ row(['Dart SDK missing', 'Install Dart SDK before running `dart pub get`, `dart analyze`, `dart test`, `dart format`, and `dart pub publish --dry-run`.']),
+ row(['Flutter SDK missing', 'Install Flutter before generating a real `flutter build web --web-renderer html` fixture.']),
+ row(['pub.dev publication', 'Founder/release coordinator must approve package name, Google account, verified publisher, and credentials.']),
+ row(['CanvasKit/Skwasm coverage', 'Requires native Flutter semantics/testing path or explicit limitation for DOM scanners.']),
+ row(['Shared CLI distribution', 'Dart users still need npm/global CLI, CI Action, Docker image, or hosted worker to hide Node/browser setup.']),
+ ])),
+ section('Domain map', table(['Domain', 'State', 'S106 interpretation'], domainRows.map((r) => row(r.map(esc))))),
+ section('Domain map: accessibility, security, privacy/GDPR, performance, reliability, sustainability, SEO/AIEO/GEO, legal notices, localization/i18n, data provenance, AI/compliance where relevant', `
This heading is deliberately explicit because S106 is a cross-domain release-evidence channel. Accessibility is implemented in the fixture; every other domain is pass-through or planned until shared Ariada domain packages mature.
`),
+ section('Flutter web evidence decision matrix', table(['Decision point', 'Recommended S106 position', 'Why it matters'], [
+ row(['HTML-renderer or semantics-rich output', 'Treat as the best current target for the adapter because DOM-oriented evidence can observe meaningful labels, links, text, forms, headings, landmarks, legal notices and metadata.', 'This is the path where Ariada evidence can be useful immediately after a Flutter web build. It still needs a real Flutter SDK fixture in the next pass.']),
+ row(['CanvasKit or Skwasm-heavy output', 'Mark as limited for DOM scanning and require native Flutter semantics tests, manual review, or future Ariada Flutter plugin work before compliance claims.', 'A canvas can be visually complete while exposing little ordinary HTML. The report must avoid overstating coverage.']),
+ row(['Public marketing site built in Flutter web', 'Recommend Ariada only if the rendered output exposes text, metadata, links, language, legal notices and crawlable content.', 'Marketing and SEO/AIEO/GEO buyers care about discoverability and inspectable structure, not only visual parity with mobile.']),
+ row(['Internal admin app deployed on web', 'Use Ariada as a release evidence packet for accessibility and legal-policy checks, but keep deeper workflow validation in Flutter widget and E2E tests.', 'Admin teams can accept CI evidence, but they still need keyboard, focus, modal, form and state-path tests outside a static scan.']),
+ row(['Public-sector service surface', 'Require the strongest path: real build output, browser scan, screenshot, raw JSON, command log, manual reviewer sign-off and retained evidence.', 'EAA and EN 301 549 evidence is a procurement and acceptance artifact, not just a developer convenience.']),
+ row(['Mobile-only Flutter app', 'Do not sell S106. Route to future mobile/app accessibility evidence work instead.', 'The distribution channel is Flutter web. Selling it to mobile-only teams would create wrong expectations.']),
+ row(['FlutterFlow or generated Flutter web', 'Treat as adjacent future onboarding, not proof of this pub package. Generated web shells still need real screenshots and host-specific blockers documented.', 'No-code and generated-app teams may buy evidence, but packaging and support surfaces differ from pub.dev developers.']),
+ row(['CI without local Dart', 'Prefer Docker/GitHub Action/hosted worker because the adapter source alone cannot prove package behavior without Dart SDK.', 'This mirrors the current host blocker and turns it into a product packaging requirement.']),
+ row(['CI with Dart but no Flutter', 'Allow URL scanning of already served Flutter web output, but block claims about `flutter build web` integration.', 'Dart package tests can pass while the Flutter build path remains unproven.']),
+ row(['CI with Flutter SDK', 'Run `flutter build web`, preserve `build/web`, run `dart run ariada:scan --static-dir build/web`, upload raw JSON, screenshots and HTML report.', 'This is the target happy path for the next S106 validation host.']),
+ row(['Hosted scan', 'Hide Dart/Flutter/Node/browser setup and sell retention, signatures, baselines and dashboards.', 'Buyers pay to remove operational friction and keep evidence history.']),
+ row(['Native Flutter plugin', 'Future path only. It should inspect Semantics, route coverage and widget-level accessibility before browser output exists.', 'A native plugin would be a different product surface from the current thin CLI wrapper.']),
+ ])),
+ section('Renderer-specific evidence adequacy', table(['Renderer or output shape', 'Evidence classification', 'Adequacy statement'], [
+ row(['HTML-like DOM output', 'tested host surface can be meaningful', 'Ariada can inspect ordinary controls, labels, headings, language, links, legal notices, metadata, structured data and many cross-domain signals.']),
+ row(['Flutter semantics DOM layer', 'partially meaningful host surface', 'Screen-reader-oriented structure may be present, but the report must still check whether labels, roles and focus semantics appear as expected.']),
+ row(['CanvasKit canvas with minimal semantics', 'limited host surface', 'Ariada may see shell metadata and canvas element only. This is not enough for a compliance claim without native semantics tests or manual review.']),
+ row(['Skwasm output', 'limited unless semantics are exposed', 'The WebAssembly renderer changes implementation details and may require separate capture/performance evidence.']),
+ row(['Server shell plus Flutter app mount', 'mixed host surface', 'Ariada can inspect the shell, legal links, metadata and app mount, but may miss widget semantics if canvas-only.']),
+ row(['Prerendered marketing shell with Flutter islands', 'promising surface', 'Ariada can inspect the public shell while separate checks handle Flutter islands. This may be the best SEO/AIEO/GEO route.']),
+ row(['Single-page authenticated app', 'requires authenticated scan path', 'Future hosted worker or CI recipe must support auth/session setup before claims are useful.']),
+ row(['Embedded Flutter web inside another host', 'host-specific evidence needed', 'The containing CMS, Angular, React or native shell can affect layout, accessibility, CSP and asset loading.']),
+ row(['PWA installable Flutter web app', 'additional manifest and offline checks needed', 'Reliability, privacy, security and legal notice checks should include manifest, service worker, cache and update behavior.']),
+ row(['Internationalized Flutter web app', 'locale-specific evidence needed', 'A single English fixture does not prove Swedish/EU language, labels, date formats, legal notices or RTL behavior.']),
+ ])),
+ section('Buyer objections and answers', table(['Objection', 'Answer Ariada should give', 'Status today'], [
+ row(['Flutter already has accessibility APIs.', 'Yes, and Ariada should complement them by scanning the built web artifact and retaining external evidence. Native Flutter semantics checks are future work.', 'Documented.']),
+ row(['CanvasKit is not normal HTML.', 'Correct. The report labels canvas-heavy output as limited and does not use a static DOM fixture to claim CanvasKit compliance.', 'Documented with caveat fixture.']),
+ row(['Why install Node for a Dart package?', 'The wrapper is intentionally thin over the shared scanner. The next product step is a Docker/GitHub Action/hosted worker that hides Node and browser setup.', 'Open packaging gap.']),
+ row(['Why not just use Lighthouse?', 'Lighthouse is useful, but Ariada is positioned as retained multi-domain compliance evidence with raw JSON, screenshots, command logs and future signed exports.', 'Positioned in competitor map.']),
+ row(['Why pay for a wrapper?', 'Do not charge for the wrapper. Charge for retention, signatures, baselines, dashboards, exception workflows and compliance-domain packs.', 'Monetization section says this.']),
+ row(['Can this prove EAA compliance?', 'No automated scanner alone proves compliance. It creates repeatable evidence and triage artifacts for human review.', 'Self-critique section says this.']),
+ row(['Will it run in our CI?', 'Yes after Dart/Flutter/Node/browser setup exists. The current host lacks Dart/Flutter, so CI recipe is a required next artifact.', 'Host blocker documented.']),
+ row(['What about authenticated routes?', 'Not implemented in S106. Future Action/hosted worker needs session setup and route inventory.', 'Future gap.']),
+ row(['What about screenshots?', 'The evidence separates tested host surface from scan-result preview and avoids report-only proof.', 'Implemented.']),
+ row(['What if pub.dev package name is unavailable?', 'Founder/release coordinator decides final name; source currently uses `ariada` to satisfy `dart run ariada:scan` in the spec.', 'Human blocker.']),
+ row(['What about FlutterFlow users?', 'Adjacent channel. Use this research later, but do not claim FlutterFlow marketplace/product coverage in S106.', 'Scoped.']),
+ row(['What about mobile accessibility?', 'Separate channel. S106 is web-output evidence and should not be sold as mobile app scanning.', 'Scoped.']),
+ ])),
+ section('pub.dev release readiness checklist', table(['Release item', 'Why it matters', 'Current state'], [
+ row(['Package name decision', 'The command requested by the handoff is `dart run ariada:scan`, which implies package name `ariada`; pub.dev availability and brand fit must be confirmed.', 'Human blocker.']),
+ row(['Verified publisher', 'Dart docs and pub.dev help emphasize publisher identity. Ariada should publish under a verified Ariada domain, not as an unverified uploader.', 'Human blocker.']),
+ row(['License and repository metadata', 'pub.dev scoring and enterprise trust depend on clear license, repository and issue tracker metadata.', 'Present in `pubspec.yaml`; final publication still needs dry-run.']),
+ row(['Executable mapping', 'Dart package layout expects public tools in `bin/`; the package exposes `scan` for `dart run ariada:scan`.', 'Implemented in source.']),
+ row(['README install path', 'Dart users need exact commands and the shared CLI dependency explained up front.', 'Implemented.']),
+ row(['Analyzer and format', 'Dart packages should pass `dart analyze` and `dart format --output=none --set-exit-if-changed .` before publication.', 'Blocked by missing Dart SDK.']),
+ row(['Tests', 'Parser and runner tests should pass under `dart test` before publication.', 'Written, blocked by missing Dart SDK.']),
+ row(['Publish dry-run', '`dart pub publish --dry-run` catches metadata and package-shape issues before credentials are used.', 'Blocked by missing Dart SDK.']),
+ row(['Flutter example', 'A real Flutter web sample build is stronger than a static fixture and should be included before public promotion.', 'Blocked by missing Flutter SDK.']),
+ row(['CI recipe', 'The first public users should be able to copy a GitHub Action without manually composing Dart, Flutter, Node, browser and upload steps.', 'Not implemented.']),
+ row(['Security disclosure', 'The package shells out to external CLI; docs should explain no secrets are collected and where artifacts are written.', 'Partially covered; needs release review.']),
+ row(['Versioning', 'Start at 0.1.0 only after runtime gates pass. Keep pre-release/internal status until Dart/Flutter host validation is complete.', 'Source says 0.1.0; publication blocked.']),
+ ])),
+ section('CI and hosted packaging backlog', table(['Backlog item', 'Free/open-source shape', 'Paid/hosted shape'], [
+ row(['GitHub Action', 'Composite Action that installs Dart/Flutter, Node, Ariada CLI, browser runtime, runs scan, uploads artifacts.', 'Enterprise variant uploads to Ariada dashboard and enforces baseline policy.']),
+ row(['Docker image', 'Pinned image with Dart/Flutter SDK, Node, shared CLI and browser cache for reproducible CI.', 'Hosted worker maintains image updates and vulnerability response.']),
+ row(['Artifact convention', 'Raw JSON, command log, screenshots, HTML report and optional route manifest under predictable names.', 'Retention, signatures, comparisons, exception approvals and export history.']),
+ row(['Auth support', 'Document local URL and static-dir first; later add Playwright session/bootstrap hook.', 'Hosted secrets, SSO, route credentials and redacted logs.']),
+ row(['Route inventory', 'Allow a simple URL list or route manifest for public Flutter web pages.', 'Fleet scan, sitemap discovery and scheduled route coverage.']),
+ row(['Renderer detection', 'Document user-provided renderer/build mode and screenshot classification.', 'Hosted analysis flags canvas-heavy output and recommends native/manual follow-up.']),
+ row(['Baseline policy', 'CLI threshold by severity and domain.', 'Organization-level policy, waivers, expiry and audit history.']),
+ row(['Evidence signing', 'Out of scope for free wrapper.', 'Signed JSON/HTML/PDF exports for procurement and regulator packets.']),
+ row(['Remediation handoff', 'Link raw findings to source/report context.', 'Team dashboards, assignments, Jira/GitHub issues and reviewer comments.']),
+ row(['Community templates', 'Issue templates asking for renderer, Flutter version, output type and failing artifact.', 'Support workflow with retained reproduction artifacts.']),
+ ])),
+ section('Expanded domain implementation backlog', table(['Domain', 'First useful Flutter web check', 'Why this domain can sell'], [
+ row(['Accessibility', 'Labels, buttons, headings, focus order proxies, statement link, language and obvious contrast where visible.', 'EAA/WCAG pressure is the immediate buying trigger.']),
+ row(['Privacy/GDPR', 'Cookie/analytics scripts, consent link, privacy notice, local/session storage inventory and third-party endpoints.', 'Public Flutter web apps often add analytics and SDKs after the UI is built.']),
+ row(['Security', 'CSP, frame options, referrer policy, permissions policy, mixed content and risky third-party resources.', 'Platform owners already understand release gates and header evidence.']),
+ row(['Performance', 'Initial payload, CanvasKit/Skwasm asset size, long tasks, render timing, image size and third-party cost.', 'Flutter web criticism often centers on payload and startup time.']),
+ row(['Reliability', 'Blank-screen risk, missing assets, service worker failure, offline route behavior and broken links.', 'A visually blank Flutter web app can be a release blocker even when the build succeeded.']),
+ row(['Sustainability', 'Transfer size, cache policy, unused payload, third-party scripts and heavy canvas/runtime assets.', 'Large web payloads create cost and carbon narratives for public services.']),
+ row(['SEO/AIEO/GEO', 'Title, description, canonical, robots, structured data, crawlable text and AI-readable public content.', 'Canvas-heavy public pages can fail discoverability expectations.']),
+ row(['Legal notices', 'Accessibility statement, privacy policy, terms, imprint/legal notice and contact paths.', 'EU public and commercial sites need visible governance links.']),
+ row(['Localization/i18n', 'HTML lang, locale route coverage, untranslated labels, date/number formats and RTL support.', 'Sweden/EU buyers care about language obligations and procurement evidence.']),
+ row(['Data provenance', 'Dataset/source links, timestamps, update policy and generated-content source references.', 'Dashboards and public data apps need trustable source lineage.']),
+ row(['AI/compliance', 'AI disclosure, generated-content notice, human review statement and EU AI Act transparency where relevant.', 'Future compliance layer for generated guidance and AI-assisted apps.']),
+ row(['Procurement packet', 'Bundle domain results into one retained artifact with reviewer notes and sign-off state.', 'This is where free wrapper adoption becomes paid workflow.']),
+ ])),
+ section('Human interview guide', table(['Interviewee', 'Questions to ask', 'Decision this informs'], [
+ row(['Flutter web developer', 'Which renderer do you use, where do you run accessibility checks, and what would make a `dart run` scanner acceptable?', 'Local/dev-loop placement and documentation tone.']),
+ row(['Flutter team lead', 'When does web output become release-critical, and who owns CI runtime setup?', 'Whether Action/Docker path is mandatory before promotion.']),
+ row(['Accessibility reviewer', 'What evidence do you need beyond raw scanner output for a Flutter web release?', 'Report fields, screenshot classification and manual-review workflow.']),
+ row(['Platform owner', 'Would you permit a Node-backed scanner in a Dart CI pipeline if it arrived as a maintained Docker/Action?', 'Packaging solution and objection handling.']),
+ row(['Public-sector supplier', 'Which artifacts are accepted in procurement: HTML report, JSON, screenshot, command log, signed PDF, or human checklist?', 'Paid export shape.']),
+ row(['Security owner', 'Should security headers and third-party resources appear in the same Flutter web release packet?', 'Cross-domain roadmap order.']),
+ row(['Privacy/legal owner', 'Which GDPR/legal-notice checks matter before a public Flutter web app ships?', 'Privacy/legal domain content.']),
+ row(['SEO/content owner', 'Do you treat Flutter web as acceptable for public content, or only for app-like surfaces?', 'SEO/AIEO/GEO positioning.']),
+ row(['Sustainability advocate', 'Do CanvasKit payload size and runtime assets matter in procurement or public reporting?', 'Sustainability sales hook.']),
+ row(['Release coordinator', 'Would pub.dev package trust require verified publisher and signed artifacts?', 'Release checklist and publication blocker.']),
+ ])),
+ section('Competitors/channel saturation', table(['Competitor or category', 'Current strength', 'Ariada response'], competitors.map((r) => row(r.map(esc))))),
+ section('Narrow competitors by domain', table(['Domain', 'Narrow alternatives', 'S106 wedge'], [
+ row(['Accessibility', 'axe, Pa11y, Lighthouse, WAVE, Deque, BrowserStack, LambdaTest', 'Dart-shaped wrapper plus retained evidence path.']),
+ row(['Security', 'ZAP, SecurityHeaders, Observatory', 'Same artifact packet as accessibility; not a pentest replacement.']),
+ row(['Privacy/GDPR', 'Cookiebot, OneTrust, CMP tools', 'Detect/review evidence, not consent operations.']),
+ row(['Performance', 'Lighthouse, WebPageTest, Flutter DevTools', 'Release-evidence capture and trend retention.']),
+ row(['Sustainability', 'Website Carbon, Ecograder', 'Combine payload and third-party evidence with compliance packet.']),
+ row(['SEO/AIEO/GEO', 'Rich Results, Schema validator, Search Console', 'Retain shell/crawlability evidence for Flutter web releases.']),
+ ])),
+ section('Community review sources', table(['Source family', 'Roles speaking', 'Signal', 'Product implication'], communitySignals.map((r) => row(r.map(esc))))),
+ section('Signal count', table(['Pattern', 'Evidence cluster'], repeatedPatterns.map((r) => row(r.map(esc))))),
+ section('Pain mining', table(['Where to search next', 'Queries and signals to collect'], [
+ row(['Flutter GitHub issues', '`is:issue web accessibility semantics CanvasKit`, `testID Flutter web`, `HTML renderer removed accessibility`; collect blocker labels and maintainer replies.']),
+ row(['Reddit r/FlutterDev', '`Flutter web accessibility`, `CanvasKit HTML renderer`, `pub.dev package publishing`; collect production anecdotes and objections.']),
+ row(['Stack Overflow', '`flutter web semantics`, `canvaskit accessibility`, `dart run executable package`; collect recurring setup questions.']),
+ row(['HN/Lobsters', '`Flutter web production ready accessibility SEO`; collect architect objections and language for positioning.']),
+ row(['pub.dev package pages', '`accessibility`, `flutter web`, `seo`, `lighthouse`; map saturated package names and maintenance quality.']),
+ row(['No-signal searches', 'G2, Capterra, TrustRadius and Product Hunt: no strong Flutter-web-specific package buying signal found; treat as weak.']),
+ ])),
+ section('Distribution/monetization', table(['Revenue layer', 'Decision'], monetizationRows.map((r) => row(r.map(esc))))),
+ section('Sources incl community/review places where possible', table(['Source', 'Owner', 'Reliability', 'URL'], linkTableRows(externalSources))),
+ section('Local source map', table(['Local file', 'Path'], localLinks.map(([label, href]) => row([esc(label), link(href, href)])))),
+ section('Source attribution method', `
Official Flutter/Dart/pub.dev/W3C/EU sources are used for stable mechanics, standards, and publishing rules. Community-review sources are used only for objections, adoption signals, and pain-mining language. Internal Ariada PRDs and package files are used for implementation boundaries and domain roadmap fit.
`),
+ section('Self-critique and limitations', table(['What this report does not prove', 'Next proof needed'], [
+ row(['It does not prove a real Flutter SDK build on this host.', 'Install Flutter and run `flutter build web --web-renderer html` against an example app.']),
+ row(['It does not prove CanvasKit accessibility completeness.', 'Build a native Flutter semantics/testing connector or mark CanvasKit as limited for DOM scans.']),
+ row(['It does not prove pub.dev name availability.', 'Release coordinator checks pub.dev and verified publisher setup.']),
+ row(['It does not prove buyer willingness.', 'Interview platform owners, accessibility reviewers, and public-sector suppliers.']),
+ row(['It does not prove all domains.', 'Implement/pass through shared Ariada domain packs as they mature.']),
+ ])),
+ section('Next steps for Ariada', table(['Owner', 'Action'], [
+ row(['Adapter maintainer', 'Run Dart gates on a host with Dart SDK: `dart pub get`, `dart analyze`, `dart test`, `dart format --output=none --set-exit-if-changed .`.']),
+ row(['Flutter maintainer', 'Create real sample app and run `flutter build web --web-renderer html`; preserve build output fixture.']),
+ row(['Platform maintainer', 'Ship a GitHub Action/Docker recipe that hides Node/browser/Ariada CLI setup.']),
+ row(['Product', 'Define paid retention, baseline policy, signed export, and exception workflow for Flutter web evidence.']),
+ row(['Research', 'Run pain-mining queries monthly and update source/signal table.']),
+ ])),
+ section('Next steps for humans', table(['Human role', 'Action'], [
+ row(['Founder/release coordinator', 'Approve pub.dev package name and verified publisher.']),
+ row(['Compliance reviewer', 'Review whether fixture findings map to EAA/WCAG buyer language.']),
+ row(['Flutter expert', 'Validate CanvasKit/Skwasm limitation and semantics-layer wording.']),
+ row(['Sales/product', 'Test pricing language with platform owners and public-sector suppliers.']),
+ ])),
+ section('Human/agent handoff', table(['Handoff item', 'Status'], [
+ row(['Changed files stay under `integrations/dart-flutter-ariada`', 'Yes.']),
+ row(['Central hub files', 'Not edited by this work item per user instruction.']),
+ row(['Mascot paths', 'Not staged.']),
+ row(['Commit author', 'Alexander Brichkin (Agonist Development AB) .']),
+ ])),
+ section('Distribution/promotion', table(['Surface', 'Message'], [
+ row(['pub.dev', 'Thin Ariada evidence adapter for Flutter web builds; scanner rules live in shared CLI.']),
+ row(['GitHub README', 'Use after `flutter build web`; document renderer caveat and artifacts.']),
+ row(['Flutter community', 'Ask for feedback on CI evidence and renderer limitations, not generic accessibility claims.']),
+ row(['Public-sector procurement', 'Offer retained EAA/WCAG evidence, screenshots, raw JSON, command log, and signed exports.']),
+ ])),
+ narrativeBlock('What developers should not be asked to own', 'Flutter web teams should not own browser-runtime caching, Node-based scanner installation, evidence signing, retention, or cross-domain policy interpretation. The wrapper should make the first local run easy; CI/Docker/hosted paths should absorb operational friction.'),
+ narrativeBlock('Future native path', 'A truly native Flutter path would inspect Flutter semantics tests, widget trees, route maps, and generated web output together. S106 does not do that. The current channel is intentionally a thin evidence bridge around built web output and the shared Ariada CLI.'),
+ narrativeBlock('Buyer timing', 'Ariada should enter when a Flutter web app becomes public-facing, contractual, regulated, or procurement-reviewed. Pure mobile teams are not the buyer for this channel until they ship a web surface.'),
+ narrativeBlock('Report-only screenshot warning', 'A report-only screenshot is useful for presentation but cannot prove the tested host surface. This report embeds and links the tested-host PNG and separately captures the scan-result preview.'),
+ narrativeBlock('Host blocker exactness', 'The host blocker is concrete: `command -v dart` and `command -v flutter` return no executable in this worktree environment. That blocks Dart/package runtime gates and real Flutter build validation, but not static source review, fixture inspection, screenshot capture, or report audit.'),
+ section('Acceptance evidence still needed before public promotion', table(['Evidence gap', 'Why it matters for S106', 'Concrete next proof'], [
+ row(['Real Flutter SDK build', 'A static fixture can prove the adapter and report path, but a public pub.dev announcement should show an actual Flutter project built with the documented renderer mode.', 'Create a tiny Flutter web app, run `flutter build web --web-renderer html` or the current supported equivalent, commit the generated representative fixture, and scan that output.']),
+ row(['Renderer/version matrix', 'Flutter web renderer behavior changes across releases. A one-version result can become stale if HTML renderer support, CanvasKit semantics, or Skwasm defaults change.', 'Record Flutter version, Dart version, renderer/build mode, generated files, and screenshot classification in each evidence bundle.']),
+ row(['Hosted CI proof', 'The product promise is stronger when Dart/Flutter/Node/browser setup is hidden from application developers.', 'Run the same fixture in a pinned Docker or GitHub Action environment and upload the full evidence bundle as an artifact.']),
+ row(['Reviewer acceptance', 'The buyer is often an accessibility or compliance reviewer, not the developer who adds the package.', 'Ask reviewers whether raw JSON, command log, tested-host screenshot, scan-result preview and HTML report are sufficient for triage, and what signed export format they require.']),
+ ])),
+];
+
+const previewRows = findings.map((finding) => row([
+ esc(finding.ruleId ?? 'unknown'),
+ esc(finding.severity ?? 'moderate'),
+ esc(finding.message ?? ''),
+]));
+
+const styles = `
+ :root { color-scheme: light; --ink: #162126; --muted: #526066; --line: #cfd8d3; --accent: #0e6f78; --soft: #eef5f1; --warn: #8a5a00; --bad: #9c2f2f; --ok: #1f6b43; }
+ html, body { overflow: hidden; }
+ ::-webkit-scrollbar { display: none; width: 0; height: 0; }
+ body { margin: 0; font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--ink); background: #fbfbf8; line-height: 1.55; }
+ header { padding: 36px 24px; background: #e8f1ed; border-bottom: 1px solid var(--line); }
+ main { max-width: 1120px; margin: 0 auto; padding: 28px 24px 56px; }
+ h1 { font-size: 34px; line-height: 1.15; margin: 0 0 10px; }
+ h2 { margin-top: 34px; padding-top: 20px; border-top: 1px solid var(--line); font-size: 22px; }
+ p { max-width: 86ch; }
+ table { width: 100%; border-collapse: collapse; margin: 14px 0 22px; font-size: 14px; background: white; }
+ th, td { border: 1px solid var(--line); padding: 9px 10px; vertical-align: top; text-align: left; }
+ th { background: var(--soft); font-weight: 700; }
+ a { color: #075e68; }
+ pre { overflow: hidden; white-space: pre-wrap; overflow-wrap: anywhere; padding: 14px; background: #172126; color: #f5fbf8; border-radius: 6px; }
+ code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
+ .badge { display: inline-block; border-radius: 999px; padding: 2px 8px; font-size: 12px; font-weight: 700; }
+ .badge.ok { background: #dff2e7; color: var(--ok); }
+ .badge.warn { background: #fff1cf; color: var(--warn); }
+ .badge.info { background: #e4edf7; color: #24537a; }
+ .hero-grid { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(280px, .8fr); gap: 24px; align-items: start; }
+ .shot { display: block; max-width: 100%; border: 1px solid var(--line); border-radius: 6px; background: white; }
+ .meta { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
+ .pill { background: white; border: 1px solid var(--line); border-radius: 999px; padding: 4px 10px; font-size: 13px; }
+ @media (max-width: 760px) { .hero-grid { grid-template-columns: 1fr; } h1 { font-size: 28px; } table { font-size: 13px; } }
+`;
+
+const previewHtml = `
+Ariada Flutter web scan preview
+
Ariada Flutter web scan-result preview
Classification: scan-result preview. This is not the tested host surface; it renders the representative shared CLI JSON for screenshot capture.
Dash-style channel evidence report for a thin Dart adapter around the shared Ariada scanner CLI. The report is intentionally explicit about the Flutter web renderer caveat, host blockers, tested surface classification, monetization path, and community-review evidence.
'}
+
+
+`;
+
+writeFileSync(join(evidenceDir, 'result.html'), reportHtml);
+console.log(`wrote ${join(evidenceDir, 'result.html')}`);
diff --git a/integrations/dart-flutter-ariada/scripts/validate-screenshots.mjs b/integrations/dart-flutter-ariada/scripts/validate-screenshots.mjs
new file mode 100644
index 00000000..66a4027c
--- /dev/null
+++ b/integrations/dart-flutter-ariada/scripts/validate-screenshots.mjs
@@ -0,0 +1,45 @@
+#!/usr/bin/env node
+// SPDX-FileCopyrightText: 2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+
+import { readFileSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const scriptDir = dirname(fileURLToPath(import.meta.url));
+const integration = dirname(scriptDir);
+const files = [
+ join(integration, 'scan-evidence/screenshots/tested-host-surface.png'),
+ join(integration, 'scan-evidence/screenshots/scan-result.png'),
+];
+
+function pngInfo(buffer) {
+ if (buffer.toString('ascii', 1, 4) !== 'PNG') throw new Error('not a PNG');
+ const width = buffer.readUInt32BE(16);
+ const height = buffer.readUInt32BE(20);
+ const chunks = [];
+ let offset = 8;
+ while (offset + 8 < buffer.length) {
+ const length = buffer.readUInt32BE(offset);
+ const type = buffer.toString('ascii', offset + 4, offset + 8);
+ const dataStart = offset + 8;
+ chunks.push({ type, dataStart, length });
+ offset = dataStart + length + 4;
+ if (type === 'IEND') break;
+ }
+ const idatBytes = chunks
+ .filter((chunk) => chunk.type === 'IDAT')
+ .reduce((total, chunk) => total + chunk.length, 0);
+ return { width, height, idatBytes };
+}
+
+for (const file of files) {
+ const info = pngInfo(readFileSync(file));
+ if (info.width < 320 || info.height < 240) {
+ throw new Error(`${file} has too-small dimensions ${info.width}x${info.height}`);
+ }
+ if (info.idatBytes < 2048) {
+ throw new Error(`${file} has too little image data (${info.idatBytes} IDAT bytes), likely blank`);
+ }
+ console.log(`${file}: ${info.width}x${info.height}, IDAT ${info.idatBytes} bytes`);
+}
diff --git a/integrations/dart-flutter-ariada/test-report/result.html b/integrations/dart-flutter-ariada/test-report/result.html
new file mode 100644
index 00000000..f7d1d950
--- /dev/null
+++ b/integrations/dart-flutter-ariada/test-report/result.html
@@ -0,0 +1,37 @@
+
+Ariada Flutter web scan preview
+
Ariada Flutter web scan-result preview
Classification: scan-result preview. This is not the tested host surface; it renders the representative shared CLI JSON for screenshot capture.
+
Rule
Severity
Message
ariada/images/alt-text
serious
Fixture image has no alt text.
+
ariada/forms/label
serious
Email input has no associated label.
+
ariada/buttons/name
moderate
Button has no accessible name.
+
ariada/statement/page-link-from-footer
moderate
Accessibility statement link is missing from the footer.
Host blocker
$ dart run ariada:scan --static-dir fixtures/flutter-web-html-renderer/build/web --domains accessibility --severity-threshold moderate --output-dir scan-evidence/ariada-output --ariada-bin ariada-stub
+Host blocker: this workstation has no Dart or Flutter executable, so dart pub get, dart analyze, dart test, dart format, dart pub publish --dry-run, and a real flutter build web run could not execute locally.
+Adapter contract note: --static-dir serves the fixture on loopback and forwards --allow-private to the shared @ariada-org/cli; served local URLs require the explicit --allow-private wrapper flag.
+Validated instead: package source structure, fixture output path, synthetic shared-CLI JSON parser contract, screenshot capture, screenshot dimensions, nonblank pixels, and Dash-plus report audit.
+The stub evidence models the expected shared @ariada-org/cli output and demonstrates the adapter contract without reimplementing scanner rules.
+
Коротко: эта ветка добавляет тонкую интеграцию для Dash.
+Разработчик может просканировать работающий Dash/Plotly analytics app через Ariada.
+Новые accessibility rules здесь не пишутся: модуль вызывает общий scanner CLI и сохраняет
+локальный evidence по served-DOM контракту. Текущий статус:
+локально готово к review
+публикация заблокирована PyPI/account доступом.
+
+
Что такое Dash и почему это канал Ariada
+
Что такое Dash
Dash это Python-фреймворк для интерактивных аналитических web dashboards. Обычно его используют data teams: Python-код поднимает web app, пользователь видит графики, таблицы, фильтры и callback-driven UI в браузере.
+
Почему это отдельный канал
Dash app нельзя надежно проверить как статический HTML файл: страница появляется после запуска Python server, callback state и browser rendering. Поэтому канал должен уметь сканировать живой URL.
+
Конкуренты в этом канале
Прямой Python data-app/dashboard канал забит: Streamlit, Gradio, Bokeh, Panel, Voila, Shiny for Python, Taipy, Reflex, Solara, NiceGUI, плюс самописные Flask/FastAPI dashboards. Смежные, но не прямые конкуренты: Tableau, Power BI, Looker, Superset, Metabase и hosted notebook/data-app platforms.
+
Насколько канал забит
Высокая насыщенность именно на уровне “как построить dashboard”. В канале уже есть mature tools для быстрых internal apps, ML demos, production dashboards, notebook-to-app flows и low-code BI. Но это не значит, что забит наш узкий канал: repeatable compliance/evidence layer для уже существующих Dash apps. Поэтому dash-ariada нельзя позиционировать как еще один dashboard framework; его позиция узкая: доказательная проверка живого Dash URL перед публикацией.
+
Какой рынок считаем
Не весь BI/analytics market. Здесь считается узкий Python dashboard / data-app developer-tool market: библиотеки, которые ставятся через PyPI и помогают Python-командам превращать data code в browser app.
+
Доля Dash по proxy
По PyPI downloads за последний месяц на 2026-06-23: Dash ≈ 8.95M. В выбранной peer group из 11 Python dashboard/data-app пакетов суммарно ≈ 59.87M, значит Dash ≈ 14.9% по download proxy. Это не настоящая market share: PyPI downloads включают CI, bots, mirrors filtering limits и transitive installs.
Не продавать Ariada как еще один способ строить dashboards. Это плохая позиция: покупатель уже выбрал Dash, Streamlit, Gradio, Power BI или Tableau, а смена framework стоит дорого и политически болезненна. Правильный wedge: “у вас уже есть Dash dashboard; добавьте повторяемый evidence layer в CI/release, чтобы доказать accessibility, security, privacy и другие compliance свойства перед публикацией”. Тогда Ariada становится не конкурентом Dash, а safety/compliance overlay поверх существующего Dash estate. Это снижает friction: разработчик не переписывает app, CI owner добавляет один scan step, reviewer получает артефакты, compliance owner получает audit trail. Первая версия должна выигрывать не красотой dashboard builder UX, а надежностью evidence: raw JSON, command log, screenshot, stable report, ссылки на PRD/docs/hub, повторяемость в pipeline. После этого расширение идет доменами: accessibility сначала, затем security/privacy для release-risk, затем sustainability/AI-readiness/structured-data для публичных и ESG/SEO-heavy dashboards.
Первый поисковый пользователь — Python/Dash developer или CI/platform owner: им проще всего поставить PyPI package и добавить один scan step. Экономический покупатель позже — data product owner, compliance/accessibility lead или DPO/legal ops, когда evidence становится обязательным release/procurement artifact.
+
Как будет распространяться
Основной низкофрикционный путь: PyPI package dash-ariada для разработчика. Коммерческий путь: CI snippets, hosted evidence retention, audit/export workflow and enterprise policy gates для platform/compliance buyer. Дополнительно: README, docs site page, Delivery Hub status row, examples для CI и snippets для Dash apps.
+
Что пользователь должен получить и зачем
Не “файлики ради файликов”, а release/review pack: команда dash-ariada scan <url> запускает проверку, raw JSON нужен CI/automation, command log нужен воспроизводимости, HTML report нужен reviewer-у, screenshot нужен человеку в ticket/PR, а hosted artifact retention нужен платящему compliance/platform owner-у как audit trail.
+
+
Channel culture fit: что любит и отвергает Dash/Python-аудитория
+
Dash-аудитория принимает Python/PyPI entrypoint, короткую локальную CLI-команду, pytest/CI automation
+и browser evidence, если scanner запускается явно перед review или release. Она хуже принимает тяжелый
+ручной bootstrap, неявные Node/browser downloads в каждом notebook run и замену выбранного dashboard framework.
+Поэтому dash-ariada должен оставаться thin PyPI adapter over Ariada CLI: local developer может
+запустить scan осознанно, CI/platform owner кеширует browser/runtime dependencies, а платный слой продает
+retention, policy gates, signed exports and multi-domain dashboard evidence.
+
+
Кому что продаем: роли, hooks, кто платит и что уже готово
+
Стартовать надо не с абстрактного “пользователя”, а с adoption path. Первый hook — Python/Dash developer,
+потому что он может поставить PyPI package и показать локальный evidence. Второй hook — CI/platform owner,
+потому что он превращает разовую проверку в release gate. Деньги появляются у data product owner,
+accessibility/compliance lead and DPO/legal ops, когда artifacts становятся audit trail and release/procurement evidence.
+
Роль
Что им обещаем
Что предлагаем
Кто платит
Когда заходим
Реализация / blockers
Python/Dash developer
“Поставь пакет и проверь dashboard до review.”
PyPI install, dash-ariada scan <url>, local HTML report, raw JSON.
Обычно не платит сам; он adoption hook.
Начинаем здесь: самый быстрый вход, потому что developer controls code/CI and can install PyPI package.
частично реализовано: local CLI, report, screenshot. Блокер: PyPI publication.
+
CI / platform owner
“Добавь gate в release pipeline и сохрани artifacts.”
Не стартуем с него cold: ему нужен уже работающий developer/CI workflow. Подключаем, когда есть recurring evidence and multi-domain coverage.
не реализовано: hosted retention, SSO, signed exports, privacy/security evidence on Dash. Это срочный commercial-product слой.
+
+
Порядок расширения доменов Ariada для Dash
+
Здесь “домен” означает compliance/analysis area Ariada, а не website domain.
+Текущая repo-карта доменов подтверждена через packages/ariada-test-fixtures/fixtures/domains/domains-index.json
+и P0/P1-P6 PRD; расширенная продуктовая карта взята из standards/platform/patent docs. Performance теперь заведен как planned D07,
+но еще не реализован в code/fixtures. Для Dash важно расширяться не “по красоте”, а по buyer pain:
+что быстрее блокирует release, создает платный enterprise use-case или делает public dashboard discoverable/compliance-ready.
+
Порядок
Домен Ariada
Почему именно так для Dash
Что делать дальше
0. Cross-domain engine / contract
P0: общий DomainModule contract, single-pass DOM walker и cross-domain interaction detector.
Без этого каждый домен будет отдельным сканером и потеряется главный moat: один scan, один report, связи между доменами.
Самая сильная стартовая боль: WCAG/EAA review gate, release blockers, аудиторам нужны доказательства. Для Dash это особенно важно, потому что app browser-rendered и часто используется в публичных/internal analytics.
Уже сделано локально: dash-ariada scan <url>, JSON/log/screenshot/report evidence.
Dash dashboards часто живут как internal tools или customer-facing analytics. Platform owner хочет знать, что release не сломал CSP/HSTS/cookies и что security policy не блокирует accessibility scripts.
Следующая версия: --domains accessibility,security demo на Dash fixture с CSP/header findings.
+
3. Privacy
Fixture index: 4 rules сейчас; PRD описывает расширение до dark-pattern checks.
Внешние dashboards могут ставить analytics/tracking cookies или собирать пользовательские фильтры/сегменты. DPO/compliance owner платит за доказательство, что pre-consent tracking и banner defects не попали в release.
Версия после security: cookie/network snapshot evidence и privacy/security interaction on cookie/request joins.
Dash apps легко становятся тяжелыми из-за graphs, JS bundles, data tables и third-party scripts. ESG/CSRD buyer слабее, чем accessibility/security, но strong differentiator для public-sector и enterprise ESG reporting.
Добавлять после privacy для public dashboards; раньше, если customer продает ESG/digital sustainability.
Важно только для public dashboards/data portals. Internal dashboards usually no. Сильная история: JS-only Dash rendering может вредить и accessibility, и AI crawler visibility.
Добавлять как public-dashboard upsell: “your data portal is accessible and AI-citable”.
Для обычного internal Dash это низкий приоритет. Для public reports, product analytics portals, dataset catalogs и investor/research pages это SEO/AI-readiness support layer.
Добавлять последним или вместе с AI readiness для public data portals.
Для Dash это реальная боль: heavy graphs, callback lag, large tables, layout shifts. Но домен нельзя заявлять shipped, пока нет performanceDomain, fixtures and CLI evidence.
Следующее: создать packages/core-engine/src/domains/performance.ts, fixtures, tests and dash-ariada scan --domains accessibility,performance evidence.
+
8. SEO
planned / PRD-backed. Внутренние документы Ariada уже выделяют SEO как отдельный домен: canonical, meta description, sitemap, robots, OG/Twitter, hreflang and JSON-LD hygiene.
Для internal Dash низкий приоритет; для public dashboards, data portals, investor dashboards and published reports это discoverability/revenue pain.
Создать D08 SEO domain PRD или привязать к L6: fixtures for public Dash report pages, canonical/meta/OG/sitemap/robots checks, and source-aware fix mapping.
+
9. GEO / AIEO
planned / PRD-backed. L6 GEO/AIEO PRD описывает AI crawler policy, llms.txt, citation/readability, AI answer visibility and content-quality scoring.
Для public data portals Dash может стать “AI-citable dashboard”: не просто доступен человеку, но понятен ChatGPT/Perplexity/Gemini/Claude and correctly summarized.
Создать Dash public-data fixture: robots/llms.txt, chunk anchors, dataset summary, AI-crawler policy, citation-ready metadata; не смешивать с shipped AI-readiness без отдельного evidence.
+
10. i18n / localization
planned / standards-backed. Multi-domain standards mapping уже включает i18n Localization.
EU/public-sector dashboards often need language, date/number/currency, RTL and translated label evidence; this overlaps accessibility and trust.
Создать D10 i18n domain PRD, multilingual Dash fixture, locale/date/currency checks and hreflang interaction with SEO.
+
11. PCI / payment
conditional planned. Multi-domain standards mapping включает PCI DSS, but only applies if payment/card flow exists.
Большинство Dash dashboards не принимают платежи. Но embedded paid reports, checkout-like upgrade panels or billing portals need payment-surface evidence.
Mark not applicable by default for analytics-only Dash. Build only if Dash channel includes billing/checkout surface; otherwise keep as conditional domain.
+
12. Jurisdiction / penalty exposure
platform-backed candidate. PLATFORM_SPEC describes jurisdiction rate cards and fine exposure estimation.
Compliance buyer wants not only “finding exists”, but “which jurisdictions and penalties matter for this dashboard audience”.
Connect report metadata to geography/audience config and penalty estimator. Do not compute legal advice; show risk bands and source links.
+
13. Brand / design-token compliance
platform-backed candidate. PLATFORM_SPEC names brand-token compliance as module M6.
Dash dashboards used in customer portals often drift from brand/design system: colors, contrast tokens, logos, disclaimers and chart palettes.
Build only after accessibility/performance because it depends on stable visual capture and token sources; candidate for design-system teams.
+
+
Каких доменов еще не хватает
+
Это backlog-карта, не обещание shipped functionality. Каждый домен ниже должен получить отдельный PRD/package/fixture set перед тем,
+как его можно будет показывать как готовый scanner domain.
+
Кандидат
Что проверяет
Зачем покупателю
Когда строить
Reliability / availability
Dashboard responds, no 5xx/timeouts, health URL works, critical routes load.
High for CI owner; overlaps monitoring but valuable as release evidence.
After performance, because Dash release risk is not only “page slow” but also “app did not come up”.
New candidate. Useful for public/regulated analytics dashboards, healthcare, finance and HR; not generic Dash.
Needs domain PRD and expert review. Do not automate serious fairness claims without policy model and human attestation.
+
Incident readiness / responsible disclosure
Security contact, vulnerability disclosure, status page link, outage/support contact, data correction channel.
New candidate. Helps public dashboards and customer portals where users need a path to report broken data, security issues or accessibility defects.
Could be a lightweight legal/reliability subdomain before becoming its own D-domain.
+
Procurement / vendor-risk evidence
Vendor data processing hints, subprocessors, hosting region, SOC2/ISO links, accessibility statement and DPA links.
New candidate. B2B buyer cares when Dash dashboard is embedded in a customer portal or procurement package.
Build only after legal/privacy/security because it aggregates their evidence into buyer-facing procurement pack.
+
Knowledge freshness / decision staleness
Whether a dashboard's numbers, commentary and cached extracts are too old for the decisions it supports.
New candidate and very Dash-specific: stale metrics can be more harmful than a missing meta tag.
Likely merge with data quality/provenance; needs explicit dataset freshness metadata contract.
+
+
Конкуренты именно в нашем узком compliance/evidence канале
+
Dashboard frameworks и BI platforms конкурируют только если мы ошибочно продаем Ariada как builder.
+В нашем реальном канале конкуренты другие: accessibility scanners, security scanners, privacy/CMP tools,
+sustainability checkers, AI-governance/AI-readiness tools and SEO/structured-data crawlers. Поэтому ниже карта по доменам,
+а не общий список “Dash vs Streamlit”.
Канал насыщен checker-ами, но слабее в Dash-specific CI evidence: raw JSON + command log + screenshot + stable report + PRD/docs/hub links, завязанные на уже существующий live Dash URL.
Сильный стартовый домен: EAA/WCAG pain понятен, отчет можно приложить к review, и текущий Ariada CLI уже дает работающий evidence path.
Они сильны в code/dependency/header security, но не продают единый dashboard-release evidence pack вместе с accessibility/privacy/sustainability. Для Dash важен web surface layer: CSP, cookies, mixed content, headers, third-party scripts на живой странице.
Второй домен после accessibility: CI owner уже привык к security gates, значит можно расширять тем же scan artifact, не меняя workflow.
CMP vendors управляют баннерами/consent, но не всегда дают developer-friendly per-release evidence по Dash app URL. Ariada должна фиксировать cookies, trackers, consent-before-tracking defects, request/cookie joins and privacy/security interactions.
Третий домен: покупатель DPO/legal ops появляется только когда report доказывает, что release не нарушил consent/privacy posture.
+
Sustainability / digital carbon / WSG evidence
Website Carbon Calculator, Ecograder, Lighthouse performance proxies, Green Web Foundation checks, EcoIndex, Wholegrain-style audits.
Эти tools дают sustainability score или carbon estimate, но не показывают конфликт “accessibility fix увеличил page weight” внутри одного compliance report. Для Dash это особенно важно из-за тяжелых graphs, tables, JS bundles and data payloads.
Четвертый домен: не главный blocker release, но хороший enterprise/public-sector differentiator and ESG story.
+
AI Act / AI transparency / AI-readiness evidence
TrustArc/OneTrust AI governance, Credo AI, Holistic AI, model governance suites, crawler/SEO tools, emerging llms.txt/AI visibility checkers.
AI governance suites работают на policy/model inventory уровне. Ariada для Dash должна быть web-surface evidence: AI-generated labels, robots/llms.txt, crawlability, structured summaries, whether JS-only dashboards are readable/citable by AI crawlers.
Пятый домен: высокий strategic upside для public dashboards/data portals, но не каждый internal Dash app имеет AI Act exposure.
+
Structured data / SEO / data portal discoverability
Google Rich Results Test, Schema.org validators, Screaming Frog, Semrush/Ahrefs site audits.
Они сильны в SEO/site crawl. Ariada должна использовать structured-data как часть multi-domain evidence: “этот public dashboard is accessible, crawlable, has machine-readable dataset/report metadata”.
Шестой домен: нужен для public data portals, investor/research dashboards and AI-readiness, но low priority for internal dashboards.
+
Traditional SEO evidence
Semrush, Ahrefs, Moz, Screaming Frog, Sitebulb, Google Search Console, Lighthouse SEO audits.
SEO suites видят site-level crawl/keywords, но usually do not attach release-level Dash CI artifacts: command log, raw JSON, screenshot, PRD link and reviewer-ready report tied to a live app URL.
Для Dash продавать не “SEO suite”, а public dashboard release evidence: canonical/meta/OG/sitemap/robots/hreflang regressions caught before publishing.
GEO tools track citations/visibility, but not necessarily source-code/CI evidence for a browser-rendered Dash dashboard. Ariada wedge: public data portal is accessible, crawlable, AI-readable and evidence-backed in one report.
Build after SEO/structured-data for public dashboards. Do not claim citation tracking until L6 capability exists.
GRC tools manage programs; Ariada can connect concrete findings to jurisdiction-aware risk bands in the evidence report, especially accessibility/privacy/AI disclosure.
Useful for buyer conversation, but must avoid legal-advice claims.
They manage brand/content rules; Ariada can check rendered dashboard artifacts against brand tokens, stale owner metadata, disclaimers, broken links and review dates.
Candidate domain for public dashboards and customer portals; not first release gate.
+
Dash-specific gap
Dash/Plotly, Streamlit, Gradio, Panel, Bokeh, Voila and BI tools mostly sell building/hosting/sharing dashboards, not compliance evidence across these domains.
Это и есть wedge: не “лучший dashboard builder”, а “один compliance/evidence overlay поверх уже выбранного dashboard estate”.
Продуктово это защищает от framework wars: Ariada добавляют после выбора Dash, а не вместо Dash.
+
+
Мэп на готовые механизмы Ariada и срочные пробелы
+
Статус
Механизм
Что это значит для Dash
Следующее действие
Already working now
dash-ariada scan <url> -> shared @ariada-org/cli -> multi-domain-report.json / raw log / screenshot / HTML report.
Этого хватает для accessibility v0 review evidence по served Dash-like surface.
Сохранить как главный contract для всех будущих доменов.
+
Already in core / ready to expose
CLI parser already documents --domains accessibility,privacy,security,sustainability,structured-data,ai-readiness; built-in domains are registered in core discovery.
Dash adapter сейчас не делает отдельный domain selection UX; он может просто прокинуть аргументы в общий CLI.
Добавить dash-ariada scan --domains accessibility,security passthrough and tests.
+
Needs urgent implementation
Dash fixture per domain: one representative Dash app with headers/cookies/scripts/heavy charts/JSON-LD/robots cases.
Без этого отчет будет говорить о domains теоретически, но не доказывать их на Dash channel.
Создать tests/fixtures/domain_matrix_app.py или static served output fixture, плюс expected findings per domain.
+
Needs urgent implementation
Report renderer that shows domain tabs/cells, interaction findings and role-oriented “what this means” text.
Raw multi-domain JSON есть, но reviewer-facing Dash report должен объяснять accessibility/security/privacy/sustainability together.
Расширить build_evidence_reports.py from single scan summary to domain matrix summary.
+
Needs urgent implementation
CI artifact recipe: GitHub Actions / GitLab CI snippets for starting Dash app, waiting for health URL, scanning, uploading artifacts.
Dash app exists as live server, so CI contract is harder than static HTML scan.
Ship examples/github-actions.yml and examples/gitlab-ci.yml with python -m dash_app + dash-ariada scan.
+
Human/account gate
PyPI publication, real Plotly/Dash Enterprise app URL, auth-protected dashboard test.
Cannot be faked locally. It needs credentials or a chosen production-like demo.
Primary interface for CI/release. Thin wrapper over Ariada CLI. Must support domain passthrough, output dir, no-fail/fail thresholds and artifact paths.
+
Python helper
from dash_ariada import render_summary
Optional in-app status panel. Not the main product; useful for demo and local visibility, but compliance value remains in CI artifacts.
Future interface for teams already testing Dash callbacks. Should start server, wait for route, run scan, attach report path to test output.
+
GitHub/GitLab CI connector
Reusable step that starts Dash, waits on /health or configured route, runs dash-ariada scan, uploads artifacts.
Most valuable paid path: CI owner buys evidence retention and policy gates.
+
Docker connector
Container image with browser deps, Python helper and Ariada CLI pinned.
Needed for reliable headless browser runs in enterprise CI without local Playwright/Chromium drift.
+
Plotly Cloud / Dash Enterprise connector
Config-driven hosted URL scan plus optional auth/session setup.
Human/account blocked. Needed before claiming production-host evidence.
+
Evidence API connector
Upload multi-domain-report.json, screenshot and logs to hosted Ariada evidence store.
Paid layer: retention, SSO, reviewer comments, signed exports and audit trail.
+
+
Как зарабатывать на Dash channel
+
Деньги находятся не в продаже нового dashboard framework. Деньги находятся в продаже уверенности: “наш живой dashboard прошел нужные проверки, evidence сохранен, release gate повторяем, auditor видит артефакты”.
+
Роль
Кто платит / влияет
Что продаем
Какое value покупают
Разработчик Dash
Обычно не главный плательщик. Он “покупает” скорость: одна команда, локальный report, меньше ручного audit ping-pong.
Free OSS/PyPI package, docs, examples, GitHub Action snippets. Это adoption channel, не основной revenue.
Value: меньше времени на evidence preparation и fewer release surprises.
Pro Team / hosted artifacts / managed CI integration. Возможный pricing anchor из internal Ariada strategy: Pro/Team per user/site/month, ниже enterprise DXP, выше pure free OSS.
Value: controlled release gate вместо screenshots в Slack.
+
Accessibility reviewer / audit lead
Платит или влияет на покупку, когда нужен повторяемый audit pack.
Главный вывод: dash-ariada не должен соревноваться с Dash, Streamlit или Gradio как framework для создания приложений.
+Его позиция сильнее как узкий evidence/compliance layer: проверить уже существующий dashboard, сохранить scanner output,
+скриншот и report, чтобы это можно было показать reviewer-у или положить в CI artifacts.
+
Конкурент / группа
В чем силен конкурент
Наше отличие
Где лучше / где хуже
Dash
Dash строит production-grade Python dashboards и имеет собственный testing/deployment ecosystem.
dash-ariada не строит dashboard и не конкурирует за UI framework choice. Он сканирует уже запущенный Dash URL и сохраняет accessibility evidence.
Лучше для compliance/review evidence. Хуже для app authoring, layout, callbacks и deployment.
+
Streamlit
Сильный быстрый путь от Python script к data app, удобный для data scientists и AI/ML teams.
Ariada не пытается быть проще Streamlit. Отличие: не authoring speed, а repeatable scan artifacts для review gates.
Лучше в доказуемости и cross-channel scanner reuse. Хуже в интерактивном app creation UX.
+
Gradio
Сильный в ML demos, model interfaces, share links и быстрых публичных демо.
Ariada не дает model demo UI. Отличие: аудит живой поверхности и доказательства для accessibility/compliance.
Лучше для regulated release checklist. Хуже для “show model to user in seconds”.
+
Panel / Bokeh / Voila
Сильны в PyData/Jupyter, visualization ecosystem и notebook-to-app workflows.
Ariada не заменяет notebook/data workflow. Отличие: один scanner core и одинаковый evidence pattern для Dash и других channels.
Лучше как общий audit layer над разными surfaces. Хуже как dashboard composition framework.
+
Tableau / Power BI / Looker / Superset / Metabase
BI platforms решают authoring, sharing, governance и embedded analytics на platform level.
Ariada Dash helper не является BI platform. Он нужен Python-командам, у которых уже есть Dash apps и которым нужно доказательство accessibility scan.
Лучше для developer-owned Python CI. Хуже для enterprise BI governance и no-code authoring.
+
+
Мэп ролей и болей на текущую реализацию
+
Роль
Боль
Насколько закрыто
Что нужно следующей версией
Dash developer
“Перед release надо быстро проверить живой dashboard.”
частично закрыто
CLI scan есть. Не хватает init wizard, examples, callback-heavy fixture, better local report UX.
+
Accessibility reviewer
“Мне нужны raw artifacts, screenshot, logs, Diff ID, а не слова.”
хорошо закрыто локально
Есть HTML report, JSON, command log, screenshot, links. Не хватает production-host evidence и stable public docs URL.
+
CI owner
“Нужно встроить в pipeline и не ловить flaky browser failures.”
начато
Команда есть. Не хватает official GitHub Actions/GitLab snippets, Docker fixture, retry policy, artifacts upload recipe.
+
Product / release owner
“Нужно понимать, можно ли публиковать и кому это продавать.”
начато
Есть market/competitor context и blocker list. Не хватает pricing/packaging decision, PyPI release, public positioning page.
+
Founder / sales
“Где wedge и почему нас не съедят Streamlit/Gradio/Dash?”
сформулировано
Wedge: accessibility evidence layer for existing Dash apps. Не хватает proof from real customer dashboard and public demo.
+
+
Направления развития: дизайн, UX, умность, надежность
+
Направление
Что есть сейчас
Чего нет
Совет по версиям
Дизайн
Сейчас: минимальный HTML evidence report и optional render_summary().
Порядок расширения доменов для Dash и текущие rule-count proxy: accessibility 47, ai-readiness 9, security 8, structured-data 5, sustainability 5, privacy 4; performance is planned/not implemented.
Источник расширенного каталога: WSG, CWV/performance, GDPR, SEO, security, i18n, PCI DSS, EU AI Act, GEO/AIEO, jurisdiction/penalty, brand-token compliance and content governance. Это не значит, что все уже реализовано.
Показывает, что SEO/GEO-подобные проблемы уже были найдены внутри Ariada: canonical/meta/OG/JSON-LD/sitemap/robots/AI-crawler gaps. Использовано как аргумент, что public Dash dashboards тоже нуждаются в discoverability evidence.
Держать запросы в research playbook, чтобы следующий pack не начинался с нуля.
+
Signals to collect
Frequency of issues, upvotes/reactions, maintainer responses, workaround complexity, enterprise mentions, release blockers, “we moved from X to Y” comments.
Не считать один angry comment рынком. Нужны кластеры боли и цитаты, привязанные к роли.
+
+
Community review sources
+
Этот блок обязателен перед выпуском отчета. Он не заменяет официальные docs; он показывает, где реальные Dash/Python/data пользователи обсуждают боли, objections and adoption signals. Один тред не считается рынком: выводы ниже должны подтверждаться source families and repeated patterns.
+
Source / signal
Channel-specific evidence
How it changes product decisions
Source families
Signal count target: 6 source families searched for this Dash channel: Plotly official community forum, Plotly/Dash GitHub issues, Stack Overflow, Reddit BI/data-science/Python communities, adjacent competitor communities, Hacker News/search surfaces.
These are channel-specific because Dash users discuss browser-rendered analytics apps in Python/data communities, not Maven or CMS forums.
Role signals: developer/maintainer. Repeated pattern: accessibility gaps are often component-level and need rendered-browser evidence, not static source lint.
Role signals: analyst, BI practitioner, data scientist, dashboard author. Repeated pattern: framework choice and deployment politics matter; Ariada should not compete as another builder.
Role signals: dashboard framework developers and maintainers. Repeated pattern: accessibility/testing/deployment pain is not Dash-only, so Ariada can become a dashboard evidence overlay.
Role signals: technical evaluators and founders. Use as weak signal only unless repeated comments cluster around deployment/compliance pain.
+
Repeated patterns
Pattern 1: component-level accessibility gaps; Pattern 2: deployment/CI/testing friction; Pattern 3: framework-choice politics between Dash/Streamlit/BI tools; Pattern 4: public vs internal dashboard compliance expectations.
Product impact: keep dash-ariada as evidence overlay over existing Dash apps, not a dashboard builder.
+
No-signal searches
Marketplace reviews are weak for Dash because PyPI does not provide review-style discussion; use PyPI only for package/distribution facts. Private Discord/Slack communities were not used because public archived evidence is required.
Do not silently omit missing surfaces; mark them weak/no-signal and prefer public forum/issues/Stack Overflow/Reddit.
+
+
Словарь этого отчета
+
Канал
Способ попасть к пользователю. Для S93 это Python/Dash ecosystem: PyPI, Dash docs/examples, CI snippets и GitHub discovery.
+
Модуль
Код в integrations/dash-ariada/, который дает CLI и optional Dash helper. Он не заменяет scanner core.
+
Поверхность
То, что реально проверяется браузером. Здесь это served Dash-like page на localhost, потому что Dash app существует как web URL.
+
Evidence
Набор доказательств: HTML report, raw JSON, command log, exit codes и screenshot. Это нужно reviewer-у и release owner-у.
Accessibility scan helper для Dash / Plotly apps, stream S93, путь integrations/dash-ariada/.
+
Проблема
Dash dashboards являются served web applications, а не статическими документами. Команде нужен повторяемый способ сканировать rendered app URL и сохранять evidence в CI или перед release.
+
Канал поставки
Python package для PyPI плюс README и hub documentation в этом репозитории.
+
Какое ядро используется
@ariada-org/cli, общий Ariada multi-domain scanner и Playwright capture stack. Этот пакет только оборачивает общий CLI.
+
Связь с патентом
В PRD указано: none. Adapter только направляет существующий CLI на served Dash URL.
+
+
+
Пользователи, роли и боли
+
Разработчик Dash / Plotly
Боль: перед demo или release нужно быстро проверить работающий analytics app, не вытаскивая HTML вручную.
+
Владелец data product
Боль: нужно доказательство, что dashboards для сотрудников, клиентов или публичных пользователей можно отдавать на accessibility review.
+
Аудитор accessibility
Боль: нужен повторяемый CLI output, raw JSON и скриншоты, а не устное “мы проверили”.
+
Владелец CI
Боль: надо встроить проверку в pipeline после запуска app на localhost во время тестов.
+
Основатель / release owner
Ответственность: PyPI publication и доступ к реальному hosted Dash/Plotly аккаунту.
+
+
Каналы и поверхности
+
Поверхность / канал
Для чего нужен
Статус
Канал распространения
PyPI package dash-ariada.
Блокер: ждет PyPI/release credentials от человека.
+
Точка входа разработчика
Console command dash-ariada scan <app-url>.
Собран и протестирован локально.
+
Точка входа внутри app
Optional render_summary() Dash component helper.
Unit test есть; demo в реальном Dash runtime еще нужен.
Нет реального deployed Dash/Plotly app URL и account context.
+
Distribution ready
нет
Нет PyPI credentials/release approval и публичной docs-site страницы.
+
Template ready for other reports
да
Этот report теперь можно использовать как формат для остальных channels: начало с channel explanation, конец с distribution plan.
+
+
Насколько адекватен тест
+
Тест адекватен для adapter contract: он проверяет, что dash-ariada
+принимает served app URL, вызывает общий Ariada CLI, читает generated JSON report,
+не ломает локальный evidence run на найденных accessibility findings при --no-fail,
+и создает браузерный screenshot страницы evidence.
+
Тест не является полной hosted-product acceptance проверкой. Он не доказывает PyPI publishing,
+Dash Enterprise deployment, Plotly Cloud deployment, authentication flows или production dashboard
+с реальными callbacks. Для этого нужны аккаунты человека и выбранное реальное приложение.
+
Доказано
dash-ariada scan <url> запускается, передает URL в общий Ariada CLI, получает scanner output и сохраняет evidence artifacts.
+
Доказано
Локальная served surface отрабатывает как browser-rendered target, а не как статический markdown/report без браузера.
+
Не доказано
PyPI install из публичного registry, потому что публикация требует credentials и human release approval.
+
Не доказано
Работа против настоящего Dash Enterprise / Plotly Cloud app с auth, callbacks, routing и production data state.
+
Следующий сильный тест
Взять реальный deployed Dash app URL, прогнать dash-ariada scan, приложить новый screenshot, raw JSON и command log как отдельный production-host evidence run.
Пересобрать остальные scan-evidence/result.html в таком же reviewer-ready виде: роли, боли, статус реализации, ядро, проверенная поверхность, адекватность теста и следующие действия.
+
Добавить public docs page после acceptance
Создать или привязать docs-site страницу для Dash usage, если канал утверждается к публикации.
+
Запустить real host demo, когда будет аккаунт
Просканировать реальный deployed Dash или Plotly app URL и приложить отдельные screenshots/logs как дополнительный evidence run.
+
+
+
Что должен сделать человек дальше
+
+
Ревью отчета
Дать правки по отчету и positioning. Аппрув commit не нужен для research/report-only изменений; approval gate нужен только для публикации, public push, release artifact или human-attributed commit.
+
Решение по публикации
Дать PyPI credentials или решить, что adapter пока остается только в repository.
+
Реальная Dash цель
Дать deployed Dash/Plotly app URL, если перед публикацией нужен production-host evidence.
+
+
+
Кто чего ждет дальше
+
Агент ждет от человека
Review comments по отчету, PyPI decision, реальный Dash/Plotly URL для production-host evidence. Аппрув нужен только если публикуем/пушим release artifact или просим подписать human-attributed commit.
+
Человек ждет от агента
Применить этот формат к остальным reports, не подменять реальные evidence screenshots synthetic previews, держать ссылки на PRD/docs/hub рядом с каждым report.
+
Release owner ждет от продукта
Понятный public positioning: “scan live Dash dashboards for multi-domain compliance evidence in CI”.
+
Reviewer ждет от report
Открыть один файл и увидеть что за канал, что проверено, что заблокировано, где raw evidence, какие домены покрыты, и что реально надо решить человеку.
+
+
Дальнейшая дистрибуция и продвижение
+
Перед публикацией
Получить PyPI credentials, выбрать package owner, подтвердить имя dash-ariada, прогнать hosted Dash/Plotly app evidence на реальном URL.
+
Документация
Добавить публичную docs-site страницу: quick start, CI example, Dash app example, что сохраняется в evidence, limitations и ссылка на этот report как образец.
+
Где рекламировать
GitHub topics и README: dash, plotly-dash, python, accessibility, wcag, ci, compliance, dashboard-testing. После PyPI: PyPI long description, docs changelog, GitHub release notes.
+
Кому показывать
Data engineering teams, analytics teams, accessibility consultants, maintainers of internal dashboards, public-sector digital teams, teams with WCAG review gates before release.
+
Следующий commit от агента
Сделать такой же reviewer-ready report template для остальных channels, чтобы каждый report начинался с описания канала и заканчивался дистрибуцией.
+
Следующее действие человека
Одобрить или отклонить review packet, дать PyPI/account доступы или явно отметить канал как repository-only до появления release credentials.
+
+
Generated from integrations/dash-ariada/scripts/build_evidence_reports.py.
+Этот отчет специально длиннее raw scan report, чтобы reviewer без внутреннего контекста видел,
+что существует, чего не хватает, кто владелец следующего действия и достаточно ли сильный evidence.
+
+
\ No newline at end of file
diff --git a/integrations/dash-ariada/scan-evidence/scan-result-preview.html b/integrations/dash-ariada/scan-evidence/scan-result-preview.html
new file mode 100644
index 00000000..54e2a975
--- /dev/null
+++ b/integrations/dash-ariada/scan-evidence/scan-result-preview.html
@@ -0,0 +1,376 @@
+
+
+
+
+
+Ariada Dash real scan preview
+
+
+
+
Ariada Dash real scan preview
+
+
Real Ariada CLI scan triggered through dash-ariada scan http://127.0.0.1:<fixture-port>.
+
12 finding(s) in scan-evidence/ariada-output/multi-domain-report.json.
Dash это Python-фреймворк для интерактивных аналитических web dashboards. Обычно его используют data teams: Python-код поднимает web app, пользователь видит графики, таблицы, фильтры и callback-driven UI в браузере.
",
+ "
Почему это отдельный канал
Dash app нельзя надежно проверить как статический HTML файл: страница появляется после запуска Python server, callback state и browser rendering. Поэтому канал должен уметь сканировать живой URL.
",
+ "
Конкуренты в этом канале
Прямой Python data-app/dashboard канал забит: Streamlit, Gradio, Bokeh, Panel, Voila, Shiny for Python, Taipy, Reflex, Solara, NiceGUI, плюс самописные Flask/FastAPI dashboards. Смежные, но не прямые конкуренты: Tableau, Power BI, Looker, Superset, Metabase и hosted notebook/data-app platforms.
",
+ "
Насколько канал забит
Высокая насыщенность именно на уровне “как построить dashboard”. В канале уже есть mature tools для быстрых internal apps, ML demos, production dashboards, notebook-to-app flows и low-code BI. Но это не значит, что забит наш узкий канал: repeatable compliance/evidence layer для уже существующих Dash apps. Поэтому dash-ariada нельзя позиционировать как еще один dashboard framework; его позиция узкая: доказательная проверка живого Dash URL перед публикацией.
",
+ "
Какой рынок считаем
Не весь BI/analytics market. Здесь считается узкий Python dashboard / data-app developer-tool market: библиотеки, которые ставятся через PyPI и помогают Python-командам превращать data code в browser app.
",
+ "
Доля Dash по proxy
По PyPI downloads за последний месяц на 2026-06-23: Dash ≈ 8.95M. В выбранной peer group из 11 Python dashboard/data-app пакетов суммарно ≈ 59.87M, значит Dash ≈ 14.9% по download proxy. Это не настоящая market share: PyPI downloads включают CI, bots, mirrors filtering limits и transitive installs.
Не продавать Ariada как еще один способ строить dashboards. Это плохая позиция: покупатель уже выбрал Dash, Streamlit, Gradio, Power BI или Tableau, а смена framework стоит дорого и политически болезненна. Правильный wedge: “у вас уже есть Dash dashboard; добавьте повторяемый evidence layer в CI/release, чтобы доказать accessibility, security, privacy и другие compliance свойства перед публикацией”. Тогда Ariada становится не конкурентом Dash, а safety/compliance overlay поверх существующего Dash estate. Это снижает friction: разработчик не переписывает app, CI owner добавляет один scan step, reviewer получает артефакты, compliance owner получает audit trail. Первая версия должна выигрывать не красотой dashboard builder UX, а надежностью evidence: raw JSON, command log, screenshot, stable report, ссылки на PRD/docs/hub, повторяемость в pipeline. После этого расширение идет доменами: accessibility сначала, затем security/privacy для release-risk, затем sustainability/AI-readiness/structured-data для публичных и ESG/SEO-heavy dashboards.
Первый поисковый пользователь — Python/Dash developer или CI/platform owner: им проще всего поставить PyPI package и добавить один scan step. Экономический покупатель позже — data product owner, compliance/accessibility lead или DPO/legal ops, когда evidence становится обязательным release/procurement artifact.
",
+ "
Как будет распространяться
Основной низкофрикционный путь: PyPI package dash-ariada для разработчика. Коммерческий путь: CI snippets, hosted evidence retention, audit/export workflow and enterprise policy gates для platform/compliance buyer. Дополнительно: README, docs site page, Delivery Hub status row, examples для CI и snippets для Dash apps.
",
+ "
Что пользователь должен получить и зачем
Не “файлики ради файликов”, а release/review pack: команда dash-ariada scan <url> запускает проверку, raw JSON нужен CI/automation, command log нужен воспроизводимости, HTML report нужен reviewer-у, screenshot нужен человеку в ticket/PR, а hosted artifact retention нужен платящему compliance/platform owner-у как audit trail.
",
+ ]
+ )
+ role_offer_rows = "\n".join(
+ [
+ "
Python/Dash developer
“Поставь пакет и проверь dashboard до review.”
PyPI install, dash-ariada scan <url>, local HTML report, raw JSON.
Обычно не платит сам; он adoption hook.
Начинаем здесь: самый быстрый вход, потому что developer controls code/CI and can install PyPI package.
частично реализовано: local CLI, report, screenshot. Блокер: PyPI publication.
",
+ "
CI / platform owner
“Добавь gate в release pipeline и сохрани artifacts.”
Получить PyPI credentials, выбрать package owner, подтвердить имя dash-ariada, прогнать hosted Dash/Plotly app evidence на реальном URL.
",
+ "
Документация
Добавить публичную docs-site страницу: quick start, CI example, Dash app example, что сохраняется в evidence, limitations и ссылка на этот report как образец.
",
+ "
Где рекламировать
GitHub topics и README: dash, plotly-dash, python, accessibility, wcag, ci, compliance, dashboard-testing. После PyPI: PyPI long description, docs changelog, GitHub release notes.
",
+ "
Кому показывать
Data engineering teams, analytics teams, accessibility consultants, maintainers of internal dashboards, public-sector digital teams, teams with WCAG review gates before release.
",
+ "
Следующий commit от агента
Сделать такой же reviewer-ready report template для остальных channels, чтобы каждый report начинался с описания канала и заканчивался дистрибуцией.
",
+ "
Следующее действие человека
Одобрить или отклонить review packet, дать PyPI/account доступы или явно отметить канал как repository-only до появления release credentials.
Самая сильная стартовая боль: WCAG/EAA review gate, release blockers, аудиторам нужны доказательства. Для Dash это особенно важно, потому что app browser-rendered и часто используется в публичных/internal analytics.
Уже сделано локально: dash-ariada scan <url>, JSON/log/screenshot/report evidence.
Dash dashboards часто живут как internal tools или customer-facing analytics. Platform owner хочет знать, что release не сломал CSP/HSTS/cookies и что security policy не блокирует accessibility scripts.
Следующая версия: --domains accessibility,security demo на Dash fixture с CSP/header findings.
",
+ "
3. Privacy
Fixture index: 4 rules сейчас; PRD описывает расширение до dark-pattern checks.
Внешние dashboards могут ставить analytics/tracking cookies или собирать пользовательские фильтры/сегменты. DPO/compliance owner платит за доказательство, что pre-consent tracking и banner defects не попали в release.
Версия после security: cookie/network snapshot evidence и privacy/security interaction on cookie/request joins.
Dash apps легко становятся тяжелыми из-за graphs, JS bundles, data tables и third-party scripts. ESG/CSRD buyer слабее, чем accessibility/security, но strong differentiator для public-sector и enterprise ESG reporting.
Добавлять после privacy для public dashboards; раньше, если customer продает ESG/digital sustainability.
Важно только для public dashboards/data portals. Internal dashboards usually no. Сильная история: JS-only Dash rendering может вредить и accessibility, и AI crawler visibility.
Добавлять как public-dashboard upsell: “your data portal is accessible and AI-citable”.
Для обычного internal Dash это низкий приоритет. Для public reports, product analytics portals, dataset catalogs и investor/research pages это SEO/AI-readiness support layer.
Добавлять последним или вместе с AI readiness для public data portals.
Для Dash это реальная боль: heavy graphs, callback lag, large tables, layout shifts. Но домен нельзя заявлять shipped, пока нет performanceDomain, fixtures and CLI evidence.
Следующее: создать packages/core-engine/src/domains/performance.ts, fixtures, tests and dash-ariada scan --domains accessibility,performance evidence.
",
+ "
8. SEO
planned / PRD-backed. Внутренние документы Ariada уже выделяют SEO как отдельный домен: canonical, meta description, sitemap, robots, OG/Twitter, hreflang and JSON-LD hygiene.
Для internal Dash низкий приоритет; для public dashboards, data portals, investor dashboards and published reports это discoverability/revenue pain.
Создать D08 SEO domain PRD или привязать к L6: fixtures for public Dash report pages, canonical/meta/OG/sitemap/robots checks, and source-aware fix mapping.
",
+ "
9. GEO / AIEO
planned / PRD-backed. L6 GEO/AIEO PRD описывает AI crawler policy, llms.txt, citation/readability, AI answer visibility and content-quality scoring.
Для public data portals Dash может стать “AI-citable dashboard”: не просто доступен человеку, но понятен ChatGPT/Perplexity/Gemini/Claude and correctly summarized.
Создать Dash public-data fixture: robots/llms.txt, chunk anchors, dataset summary, AI-crawler policy, citation-ready metadata; не смешивать с shipped AI-readiness без отдельного evidence.
",
+ "
10. i18n / localization
planned / standards-backed. Multi-domain standards mapping уже включает i18n Localization.
EU/public-sector dashboards often need language, date/number/currency, RTL and translated label evidence; this overlaps accessibility and trust.
Создать D10 i18n domain PRD, multilingual Dash fixture, locale/date/currency checks and hreflang interaction with SEO.
",
+ "
11. PCI / payment
conditional planned. Multi-domain standards mapping включает PCI DSS, but only applies if payment/card flow exists.
Большинство Dash dashboards не принимают платежи. Но embedded paid reports, checkout-like upgrade panels or billing portals need payment-surface evidence.
Mark not applicable by default for analytics-only Dash. Build only if Dash channel includes billing/checkout surface; otherwise keep as conditional domain.
",
+ "
12. Jurisdiction / penalty exposure
platform-backed candidate. PLATFORM_SPEC describes jurisdiction rate cards and fine exposure estimation.
Compliance buyer wants not only “finding exists”, but “which jurisdictions and penalties matter for this dashboard audience”.
Connect report metadata to geography/audience config and penalty estimator. Do not compute legal advice; show risk bands and source links.
",
+ "
13. Brand / design-token compliance
platform-backed candidate. PLATFORM_SPEC names brand-token compliance as module M6.
Dash dashboards used in customer portals often drift from brand/design system: colors, contrast tokens, logos, disclaimers and chart palettes.
Build only after accessibility/performance because it depends on stable visual capture and token sources; candidate for design-system teams.
Канал насыщен checker-ами, но слабее в Dash-specific CI evidence: raw JSON + command log + screenshot + stable report + PRD/docs/hub links, завязанные на уже существующий live Dash URL.
Сильный стартовый домен: EAA/WCAG pain понятен, отчет можно приложить к review, и текущий Ariada CLI уже дает работающий evidence path.
Они сильны в code/dependency/header security, но не продают единый dashboard-release evidence pack вместе с accessibility/privacy/sustainability. Для Dash важен web surface layer: CSP, cookies, mixed content, headers, third-party scripts на живой странице.
Второй домен после accessibility: CI owner уже привык к security gates, значит можно расширять тем же scan artifact, не меняя workflow.
CMP vendors управляют баннерами/consent, но не всегда дают developer-friendly per-release evidence по Dash app URL. Ariada должна фиксировать cookies, trackers, consent-before-tracking defects, request/cookie joins and privacy/security interactions.
Третий домен: покупатель DPO/legal ops появляется только когда report доказывает, что release не нарушил consent/privacy posture.
",
+ "
Sustainability / digital carbon / WSG evidence
Website Carbon Calculator, Ecograder, Lighthouse performance proxies, Green Web Foundation checks, EcoIndex, Wholegrain-style audits.
Эти tools дают sustainability score или carbon estimate, но не показывают конфликт “accessibility fix увеличил page weight” внутри одного compliance report. Для Dash это особенно важно из-за тяжелых graphs, tables, JS bundles and data payloads.
Четвертый домен: не главный blocker release, но хороший enterprise/public-sector differentiator and ESG story.
",
+ "
AI Act / AI transparency / AI-readiness evidence
TrustArc/OneTrust AI governance, Credo AI, Holistic AI, model governance suites, crawler/SEO tools, emerging llms.txt/AI visibility checkers.
AI governance suites работают на policy/model inventory уровне. Ariada для Dash должна быть web-surface evidence: AI-generated labels, robots/llms.txt, crawlability, structured summaries, whether JS-only dashboards are readable/citable by AI crawlers.
Пятый домен: высокий strategic upside для public dashboards/data portals, но не каждый internal Dash app имеет AI Act exposure.
",
+ "
Structured data / SEO / data portal discoverability
Google Rich Results Test, Schema.org validators, Screaming Frog, Semrush/Ahrefs site audits.
Они сильны в SEO/site crawl. Ariada должна использовать structured-data как часть multi-domain evidence: “этот public dashboard is accessible, crawlable, has machine-readable dataset/report metadata”.
Шестой домен: нужен для public data portals, investor/research dashboards and AI-readiness, но low priority for internal dashboards.
",
+ "
Traditional SEO evidence
Semrush, Ahrefs, Moz, Screaming Frog, Sitebulb, Google Search Console, Lighthouse SEO audits.
SEO suites видят site-level crawl/keywords, но usually do not attach release-level Dash CI artifacts: command log, raw JSON, screenshot, PRD link and reviewer-ready report tied to a live app URL.
Для Dash продавать не “SEO suite”, а public dashboard release evidence: canonical/meta/OG/sitemap/robots/hreflang regressions caught before publishing.
GEO tools track citations/visibility, but not necessarily source-code/CI evidence for a browser-rendered Dash dashboard. Ariada wedge: public data portal is accessible, crawlable, AI-readable and evidence-backed in one report.
Build after SEO/structured-data for public dashboards. Do not claim citation tracking until L6 capability exists.
GRC tools manage programs; Ariada can connect concrete findings to jurisdiction-aware risk bands in the evidence report, especially accessibility/privacy/AI disclosure.
Useful for buyer conversation, but must avoid legal-advice claims.
They manage brand/content rules; Ariada can check rendered dashboard artifacts against brand tokens, stale owner metadata, disclaimers, broken links and review dates.
Candidate domain for public dashboards and customer portals; not first release gate.
",
+ "
Dash-specific gap
Dash/Plotly, Streamlit, Gradio, Panel, Bokeh, Voila and BI tools mostly sell building/hosting/sharing dashboards, not compliance evidence across these domains.
Это и есть wedge: не “лучший dashboard builder”, а “один compliance/evidence overlay поверх уже выбранного dashboard estate”.
Продуктово это защищает от framework wars: Ariada добавляют после выбора Dash, а не вместо Dash.
Primary interface for CI/release. Thin wrapper over Ariada CLI. Must support domain passthrough, output dir, no-fail/fail thresholds and artifact paths.
",
+ "
Python helper
from dash_ariada import render_summary
Optional in-app status panel. Not the main product; useful for demo and local visibility, but compliance value remains in CI artifacts.
Pro Team / hosted artifacts / managed CI integration. Возможный pricing anchor из internal Ariada strategy: Pro/Team per user/site/month, ниже enterprise DXP, выше pure free OSS.
Value: controlled release gate вместо screenshots в Slack.
",
+ "
Accessibility reviewer / audit lead
Платит или влияет на покупку, когда нужен повторяемый audit pack.
Review comments по отчету, PyPI decision, реальный Dash/Plotly URL для production-host evidence. Аппрув нужен только если публикуем/пушим release artifact или просим подписать human-attributed commit.
",
+ "
Человек ждет от агента
Применить этот формат к остальным reports, не подменять реальные evidence screenshots synthetic previews, держать ссылки на PRD/docs/hub рядом с каждым report.
",
+ "
Release owner ждет от продукта
Понятный public positioning: “scan live Dash dashboards for multi-domain compliance evidence in CI”.
",
+ "
Reviewer ждет от report
Открыть один файл и увидеть что за канал, что проверено, что заблокировано, где raw evidence, какие домены покрыты, и что реально надо решить человеку.
",
+ ]
+ )
+ term_rows = "\n".join(
+ [
+ "
Канал
Способ попасть к пользователю. Для S93 это Python/Dash ecosystem: PyPI, Dash docs/examples, CI snippets и GitHub discovery.
",
+ "
Модуль
Код в integrations/dash-ariada/, который дает CLI и optional Dash helper. Он не заменяет scanner core.
",
+ "
Поверхность
То, что реально проверяется браузером. Здесь это served Dash-like page на localhost, потому что Dash app существует как web URL.
",
+ "
Evidence
Набор доказательств: HTML report, raw JSON, command log, exit codes и screenshot. Это нужно reviewer-у и release owner-у.
Порядок расширения доменов для Dash и текущие rule-count proxy: accessibility 47, ai-readiness 9, security 8, structured-data 5, sustainability 5, privacy 4; performance is planned/not implemented.
Источник расширенного каталога: WSG, CWV/performance, GDPR, SEO, security, i18n, PCI DSS, EU AI Act, GEO/AIEO, jurisdiction/penalty, brand-token compliance and content governance. Это не значит, что все уже реализовано.
Показывает, что SEO/GEO-подобные проблемы уже были найдены внутри Ariada: canonical/meta/OG/JSON-LD/sitemap/robots/AI-crawler gaps. Использовано как аргумент, что public Dash dashboards тоже нуждаются в discoverability evidence.
Держать запросы в research playbook, чтобы следующий pack не начинался с нуля.
",
+ "
Signals to collect
Frequency of issues, upvotes/reactions, maintainer responses, workaround complexity, enterprise mentions, release blockers, “we moved from X to Y” comments.
Не считать один angry comment рынком. Нужны кластеры боли и цитаты, привязанные к роли.
Role signals: developer/maintainer. Repeated pattern: accessibility gaps are often component-level and need rendered-browser evidence, not static source lint.
Role signals: analyst, BI practitioner, data scientist, dashboard author. Repeated pattern: framework choice and deployment politics matter; Ariada should not compete as another builder.
Role signals: dashboard framework developers and maintainers. Repeated pattern: accessibility/testing/deployment pain is not Dash-only, so Ariada can become a dashboard evidence overlay.
Role signals: technical evaluators and founders. Use as weak signal only unless repeated comments cluster around deployment/compliance pain.
",
+ "
Repeated patterns
Pattern 1: component-level accessibility gaps; Pattern 2: deployment/CI/testing friction; Pattern 3: framework-choice politics between Dash/Streamlit/BI tools; Pattern 4: public vs internal dashboard compliance expectations.
Product impact: keep dash-ariada as evidence overlay over existing Dash apps, not a dashboard builder.
",
+ "
No-signal searches
Marketplace reviews are weak for Dash because PyPI does not provide review-style discussion; use PyPI only for package/distribution facts. Private Discord/Slack communities were not used because public archived evidence is required.
Do not silently omit missing surfaces; mark them weak/no-signal and prefer public forum/issues/Stack Overflow/Reddit.
",
+ ]
+ )
+ adequacy_rows = "\n".join(
+ [
+ "
Доказано
dash-ariada scan <url> запускается, передает URL в общий Ariada CLI, получает scanner output и сохраняет evidence artifacts.
",
+ "
Доказано
Локальная served surface отрабатывает как browser-rendered target, а не как статический markdown/report без браузера.
",
+ "
Не доказано
PyPI install из публичного registry, потому что публикация требует credentials и human release approval.
",
+ "
Не доказано
Работа против настоящего Dash Enterprise / Plotly Cloud app с auth, callbacks, routing и production data state.
",
+ "
Следующий сильный тест
Взять реальный deployed Dash app URL, прогнать dash-ariada scan, приложить новый screenshot, raw JSON и command log как отдельный production-host evidence run.
Коротко: эта ветка добавляет тонкую интеграцию для Dash.
+Разработчик может просканировать работающий Dash/Plotly analytics app через Ariada.
+Новые accessibility rules здесь не пишутся: модуль вызывает общий scanner CLI и сохраняет
+локальный evidence по served-DOM контракту. Текущий статус:
+локально готово к review
+публикация заблокирована PyPI/account доступом.
+
+
Что такое Dash и почему это канал Ariada
+
{dash_channel_rows}
+
+
Channel culture fit: что любит и отвергает Dash/Python-аудитория
+
Dash-аудитория принимает Python/PyPI entrypoint, короткую локальную CLI-команду, pytest/CI automation
+и browser evidence, если scanner запускается явно перед review или release. Она хуже принимает тяжелый
+ручной bootstrap, неявные Node/browser downloads в каждом notebook run и замену выбранного dashboard framework.
+Поэтому dash-ariada должен оставаться thin PyPI adapter over Ariada CLI: local developer может
+запустить scan осознанно, CI/platform owner кеширует browser/runtime dependencies, а платный слой продает
+retention, policy gates, signed exports and multi-domain dashboard evidence.
+
+
Кому что продаем: роли, hooks, кто платит и что уже готово
+
Стартовать надо не с абстрактного “пользователя”, а с adoption path. Первый hook — Python/Dash developer,
+потому что он может поставить PyPI package и показать локальный evidence. Второй hook — CI/platform owner,
+потому что он превращает разовую проверку в release gate. Деньги появляются у data product owner,
+accessibility/compliance lead and DPO/legal ops, когда artifacts становятся audit trail and release/procurement evidence.
+
Роль
Что им обещаем
Что предлагаем
Кто платит
Когда заходим
Реализация / blockers
{role_offer_rows}
+
+
Порядок расширения доменов Ariada для Dash
+
Здесь “домен” означает compliance/analysis area Ariada, а не website domain.
+Текущая repo-карта доменов подтверждена через packages/ariada-test-fixtures/fixtures/domains/domains-index.json
+и P0/P1-P6 PRD; расширенная продуктовая карта взята из standards/platform/patent docs. Performance теперь заведен как planned D07,
+но еще не реализован в code/fixtures. Для Dash важно расширяться не “по красоте”, а по buyer pain:
+что быстрее блокирует release, создает платный enterprise use-case или делает public dashboard discoverable/compliance-ready.
+
Порядок
Домен Ariada
Почему именно так для Dash
Что делать дальше
{domain_expansion_rows}
+
+
Каких доменов еще не хватает
+
Это backlog-карта, не обещание shipped functionality. Каждый домен ниже должен получить отдельный PRD/package/fixture set перед тем,
+как его можно будет показывать как готовый scanner domain.
+
Кандидат
Что проверяет
Зачем покупателю
Когда строить
{missing_domain_rows}
+
+
Конкуренты именно в нашем узком compliance/evidence канале
+
Dashboard frameworks и BI platforms конкурируют только если мы ошибочно продаем Ariada как builder.
+В нашем реальном канале конкуренты другие: accessibility scanners, security scanners, privacy/CMP tools,
+sustainability checkers, AI-governance/AI-readiness tools and SEO/structured-data crawlers. Поэтому ниже карта по доменам,
+а не общий список “Dash vs Streamlit”.
+
Домен
Кто уже силен
Где остается щель для Ariada
Вывод для Dash
{narrow_compliance_competitor_rows}
+
+
Мэп на готовые механизмы Ariada и срочные пробелы
+
Статус
Механизм
Что это значит для Dash
Следующее действие
{dash_implementation_map_rows}
+
+
Технические интерфейсы и коннекторы для Dash
+
Интерфейс
Форма
Для чего нужен
{dash_connector_rows}
+
+
Как зарабатывать на Dash channel
+
Деньги находятся не в продаже нового dashboard framework. Деньги находятся в продаже уверенности: “наш живой dashboard прошел нужные проверки, evidence сохранен, release gate повторяем, auditor видит артефакты”.
+
Роль
Кто платит / влияет
Что продаем
Какое value покупают
{monetization_rows}
+
+
Модели продаж конкурентов в канале
+
Игрок
Как зарабатывает
Что это значит для Ariada
Источники
{sales_model_rows}
+
+
Отличия от конкурентов и где мы лучше/хуже
+
Главный вывод: dash-ariada не должен соревноваться с Dash, Streamlit или Gradio как framework для создания приложений.
+Его позиция сильнее как узкий evidence/compliance layer: проверить уже существующий dashboard, сохранить scanner output,
+скриншот и report, чтобы это можно было показать reviewer-у или положить в CI artifacts.
+
Конкурент / группа
В чем силен конкурент
Наше отличие
Где лучше / где хуже
{competitor_diff_rows}
+
+
Мэп ролей и болей на текущую реализацию
+
Роль
Боль
Насколько закрыто
Что нужно следующей версией
{role_pain_fit_rows}
+
+
Направления развития: дизайн, UX, умность, надежность
+
Направление
Что есть сейчас
Чего нет
Совет по версиям
{direction_rows}
+
+
Источники и документы
+
Что подтверждает
Источник
Как использовано в отчете
{source_rows}
+
+
Где дальше искать боли, роли и отзывы
+
Направление поиска
Где искать
Что извлекать
{further_research_rows}
+
+
Community review sources
+
Этот блок обязателен перед выпуском отчета. Он не заменяет официальные docs; он показывает, где реальные Dash/Python/data пользователи обсуждают боли, objections and adoption signals. Один тред не считается рынком: выводы ниже должны подтверждаться source families and repeated patterns.
Accessibility scan helper для Dash / Plotly apps, stream S93, путь integrations/dash-ariada/.
+
Проблема
Dash dashboards являются served web applications, а не статическими документами. Команде нужен повторяемый способ сканировать rendered app URL и сохранять evidence в CI или перед release.
+
Канал поставки
Python package для PyPI плюс README и hub documentation в этом репозитории.
+
Какое ядро используется
@ariada-org/cli, общий Ariada multi-domain scanner и Playwright capture stack. Этот пакет только оборачивает общий CLI.
+
Связь с патентом
В PRD указано: none. Adapter только направляет существующий CLI на served Dash URL.
+
+
+
Пользователи, роли и боли
+
{role_rows}
+
+
Каналы и поверхности
+
Поверхность / канал
Для чего нужен
Статус
{channel_rows}
+
+
Что реализовано и что не реализовано
+
{implemented_rows}
+
+
Готовность по уровням
+
Уровень
Готово?
Почему
{readiness_rows}
+
+
Насколько адекватен тест
+
Тест адекватен для adapter contract: он проверяет, что dash-ariada
+принимает served app URL, вызывает общий Ariada CLI, читает generated JSON report,
+не ломает локальный evidence run на найденных accessibility findings при --no-fail,
+и создает браузерный screenshot страницы evidence.
+
Тест не является полной hosted-product acceptance проверкой. Он не доказывает PyPI publishing,
+Dash Enterprise deployment, Plotly Cloud deployment, authentication flows или production dashboard
+с реальными callbacks. Для этого нужны аккаунты человека и выбранное реальное приложение.
+
{adequacy_rows}
+
+
Какие gates были запущены
+
Gate
Статус
Команда
Evidence
{gate_rows}
+
+
Результат scan
+
{total} finding(s) найдено общим scanner CLI на representative served Dash-like surface.
+{shot}
+
+
Command output / сырой вывод команды
+
{esc(read(SCAN_EVIDENCE / "command.log") or "(no command output)")}
+
+
Что должен сделать агент дальше
+
+
Применить этот формат к остальным каналам
Пересобрать остальные scan-evidence/result.html в таком же reviewer-ready виде: роли, боли, статус реализации, ядро, проверенная поверхность, адекватность теста и следующие действия.
+
Добавить public docs page после acceptance
Создать или привязать docs-site страницу для Dash usage, если канал утверждается к публикации.
+
Запустить real host demo, когда будет аккаунт
Просканировать реальный deployed Dash или Plotly app URL и приложить отдельные screenshots/logs как дополнительный evidence run.
+
+
+
Что должен сделать человек дальше
+
+
Ревью отчета
Дать правки по отчету и positioning. Аппрув commit не нужен для research/report-only изменений; approval gate нужен только для публикации, public push, release artifact или human-attributed commit.
+
Решение по публикации
Дать PyPI credentials или решить, что adapter пока остается только в repository.
+
Реальная Dash цель
Дать deployed Dash/Plotly app URL, если перед публикацией нужен production-host evidence.
+
+
+
Кто чего ждет дальше
+
{handoff_rows}
+
+
Дальнейшая дистрибуция и продвижение
+
{distribution_rows}
+
+
Generated from integrations/dash-ariada/scripts/build_evidence_reports.py.
+Этот отчет специально длиннее raw scan report, чтобы reviewer без внутреннего контекста видел,
+что существует, чего не хватает, кто владелец следующего действия и достаточно ли сильный evidence.
+""",
+ ),
+ encoding="utf-8",
+ )
+
+
+def main() -> None:
+ build_test_report()
+ build_scan_preview()
+ build_scan_report()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/integrations/dash-ariada/scripts/capture_scan_screenshot.mjs b/integrations/dash-ariada/scripts/capture_scan_screenshot.mjs
new file mode 100644
index 00000000..41a1ce41
--- /dev/null
+++ b/integrations/dash-ariada/scripts/capture_scan_screenshot.mjs
@@ -0,0 +1,14 @@
+import { chromium } from 'playwright';
+import { mkdir } from 'node:fs/promises';
+import { resolve } from 'node:path';
+
+const root = resolve(import.meta.dirname, '..');
+const preview = `file://${resolve(root, 'scan-evidence/scan-result-preview.html')}`;
+const output = resolve(root, 'scan-evidence/screenshots/scan-result.png');
+
+await mkdir(resolve(root, 'scan-evidence/screenshots'), { recursive: true });
+const browser = await chromium.launch({ headless: true });
+const page = await browser.newPage({ viewport: { width: 1280, height: 960 } });
+await page.goto(preview, { waitUntil: 'load' });
+await page.screenshot({ path: output, fullPage: true });
+await browser.close();
diff --git a/integrations/dash-ariada/test-report/logs/ariada-cli-build.exit b/integrations/dash-ariada/test-report/logs/ariada-cli-build.exit
new file mode 100644
index 00000000..c2270834
--- /dev/null
+++ b/integrations/dash-ariada/test-report/logs/ariada-cli-build.exit
@@ -0,0 +1 @@
+0
\ No newline at end of file
diff --git a/integrations/dash-ariada/test-report/logs/ariada-cli-build.log b/integrations/dash-ariada/test-report/logs/ariada-cli-build.log
new file mode 100644
index 00000000..b0a613d7
--- /dev/null
+++ b/integrations/dash-ariada/test-report/logs/ariada-cli-build.log
@@ -0,0 +1,3 @@
+
+> @ariada-org/cli@0.1.0 build /Users/pedro/adopta-s93-dash/packages/ariada-cli
+> tsc -p tsconfig.json && node -e "import('node:fs').then(fs=>fs.chmodSync('dist/bin.js',0o755))"
diff --git a/integrations/dash-ariada/test-report/logs/build.exit b/integrations/dash-ariada/test-report/logs/build.exit
new file mode 100644
index 00000000..c2270834
--- /dev/null
+++ b/integrations/dash-ariada/test-report/logs/build.exit
@@ -0,0 +1 @@
+0
\ No newline at end of file
diff --git a/integrations/dash-ariada/test-report/logs/build.log b/integrations/dash-ariada/test-report/logs/build.log
new file mode 100644
index 00000000..6dba9a7f
--- /dev/null
+++ b/integrations/dash-ariada/test-report/logs/build.log
@@ -0,0 +1,106 @@
+* Creating isolated environment: venv+pip...
+* Installing packages in isolated environment:
+ - setuptools>=69
+ - wheel
+* Getting build dependencies for sdist...
+running egg_info
+writing dash_ariada.egg-info/PKG-INFO
+writing dependency_links to dash_ariada.egg-info/dependency_links.txt
+writing entry points to dash_ariada.egg-info/entry_points.txt
+writing requirements to dash_ariada.egg-info/requires.txt
+writing top-level names to dash_ariada.egg-info/top_level.txt
+reading manifest file 'dash_ariada.egg-info/SOURCES.txt'
+writing manifest file 'dash_ariada.egg-info/SOURCES.txt'
+* Building sdist...
+running sdist
+running egg_info
+writing dash_ariada.egg-info/PKG-INFO
+writing dependency_links to dash_ariada.egg-info/dependency_links.txt
+writing entry points to dash_ariada.egg-info/entry_points.txt
+writing requirements to dash_ariada.egg-info/requires.txt
+writing top-level names to dash_ariada.egg-info/top_level.txt
+reading manifest file 'dash_ariada.egg-info/SOURCES.txt'
+writing manifest file 'dash_ariada.egg-info/SOURCES.txt'
+running check
+creating dash_ariada-0.1.0
+creating dash_ariada-0.1.0/dash_ariada
+creating dash_ariada-0.1.0/dash_ariada.egg-info
+creating dash_ariada-0.1.0/tests
+copying files to dash_ariada-0.1.0...
+copying README.md -> dash_ariada-0.1.0
+copying pyproject.toml -> dash_ariada-0.1.0
+copying dash_ariada/__init__.py -> dash_ariada-0.1.0/dash_ariada
+copying dash_ariada/__main__.py -> dash_ariada-0.1.0/dash_ariada
+copying dash_ariada/cli.py -> dash_ariada-0.1.0/dash_ariada
+copying dash_ariada/component.py -> dash_ariada-0.1.0/dash_ariada
+copying dash_ariada/scanner.py -> dash_ariada-0.1.0/dash_ariada
+copying dash_ariada.egg-info/PKG-INFO -> dash_ariada-0.1.0/dash_ariada.egg-info
+copying dash_ariada.egg-info/SOURCES.txt -> dash_ariada-0.1.0/dash_ariada.egg-info
+copying dash_ariada.egg-info/dependency_links.txt -> dash_ariada-0.1.0/dash_ariada.egg-info
+copying dash_ariada.egg-info/entry_points.txt -> dash_ariada-0.1.0/dash_ariada.egg-info
+copying dash_ariada.egg-info/requires.txt -> dash_ariada-0.1.0/dash_ariada.egg-info
+copying dash_ariada.egg-info/top_level.txt -> dash_ariada-0.1.0/dash_ariada.egg-info
+copying tests/test_scanner.py -> dash_ariada-0.1.0/tests
+copying dash_ariada.egg-info/SOURCES.txt -> dash_ariada-0.1.0/dash_ariada.egg-info
+Writing dash_ariada-0.1.0/setup.cfg
+Creating tar archive
+removing 'dash_ariada-0.1.0' (and everything under it)
+* Building wheel from sdist
+* Creating isolated environment: venv+pip...
+* Installing packages in isolated environment:
+ - setuptools>=69
+ - wheel
+* Getting build dependencies for wheel...
+running egg_info
+writing dash_ariada.egg-info/PKG-INFO
+writing dependency_links to dash_ariada.egg-info/dependency_links.txt
+writing entry points to dash_ariada.egg-info/entry_points.txt
+writing requirements to dash_ariada.egg-info/requires.txt
+writing top-level names to dash_ariada.egg-info/top_level.txt
+reading manifest file 'dash_ariada.egg-info/SOURCES.txt'
+writing manifest file 'dash_ariada.egg-info/SOURCES.txt'
+* Building wheel...
+running bdist_wheel
+running build
+running build_py
+creating build/lib/dash_ariada
+copying dash_ariada/scanner.py -> build/lib/dash_ariada
+copying dash_ariada/__init__.py -> build/lib/dash_ariada
+copying dash_ariada/cli.py -> build/lib/dash_ariada
+copying dash_ariada/component.py -> build/lib/dash_ariada
+copying dash_ariada/__main__.py -> build/lib/dash_ariada
+running egg_info
+writing dash_ariada.egg-info/PKG-INFO
+writing dependency_links to dash_ariada.egg-info/dependency_links.txt
+writing entry points to dash_ariada.egg-info/entry_points.txt
+writing requirements to dash_ariada.egg-info/requires.txt
+writing top-level names to dash_ariada.egg-info/top_level.txt
+reading manifest file 'dash_ariada.egg-info/SOURCES.txt'
+writing manifest file 'dash_ariada.egg-info/SOURCES.txt'
+installing to build/bdist.macosx-10.9-universal2/wheel
+running install
+running install_lib
+creating build/bdist.macosx-10.9-universal2/wheel
+creating build/bdist.macosx-10.9-universal2/wheel/dash_ariada
+copying build/lib/dash_ariada/scanner.py -> build/bdist.macosx-10.9-universal2/wheel/./dash_ariada
+copying build/lib/dash_ariada/__init__.py -> build/bdist.macosx-10.9-universal2/wheel/./dash_ariada
+copying build/lib/dash_ariada/cli.py -> build/bdist.macosx-10.9-universal2/wheel/./dash_ariada
+copying build/lib/dash_ariada/component.py -> build/bdist.macosx-10.9-universal2/wheel/./dash_ariada
+copying build/lib/dash_ariada/__main__.py -> build/bdist.macosx-10.9-universal2/wheel/./dash_ariada
+running install_egg_info
+Copying dash_ariada.egg-info to build/bdist.macosx-10.9-universal2/wheel/./dash_ariada-0.1.0-py3.9.egg-info
+running install_scripts
+creating build/bdist.macosx-10.9-universal2/wheel/dash_ariada-0.1.0.dist-info/WHEEL
+creating '/Users/pedro/adopta-s93-dash/integrations/dash-ariada/dist/.tmp-q3qf0t4z/dash_ariada-0.1.0-py3-none-any.whl' and adding 'build/bdist.macosx-10.9-universal2/wheel' to it
+adding 'dash_ariada/__init__.py'
+adding 'dash_ariada/__main__.py'
+adding 'dash_ariada/cli.py'
+adding 'dash_ariada/component.py'
+adding 'dash_ariada/scanner.py'
+adding 'dash_ariada-0.1.0.dist-info/METADATA'
+adding 'dash_ariada-0.1.0.dist-info/WHEEL'
+adding 'dash_ariada-0.1.0.dist-info/entry_points.txt'
+adding 'dash_ariada-0.1.0.dist-info/top_level.txt'
+adding 'dash_ariada-0.1.0.dist-info/RECORD'
+removing build/bdist.macosx-10.9-universal2/wheel
+Successfully built dash_ariada-0.1.0.tar.gz and dash_ariada-0.1.0-py3-none-any.whl
diff --git a/integrations/dash-ariada/test-report/logs/compileall.exit b/integrations/dash-ariada/test-report/logs/compileall.exit
new file mode 100644
index 00000000..c2270834
--- /dev/null
+++ b/integrations/dash-ariada/test-report/logs/compileall.exit
@@ -0,0 +1 @@
+0
\ No newline at end of file
diff --git a/integrations/dash-ariada/test-report/logs/compileall.log b/integrations/dash-ariada/test-report/logs/compileall.log
new file mode 100644
index 00000000..e69de29b
diff --git a/integrations/dash-ariada/test-report/logs/evidence-report.log b/integrations/dash-ariada/test-report/logs/evidence-report.log
new file mode 100644
index 00000000..e69de29b
diff --git a/integrations/dash-ariada/test-report/logs/install.exit b/integrations/dash-ariada/test-report/logs/install.exit
new file mode 100644
index 00000000..c2270834
--- /dev/null
+++ b/integrations/dash-ariada/test-report/logs/install.exit
@@ -0,0 +1 @@
+0
\ No newline at end of file
diff --git a/integrations/dash-ariada/test-report/logs/install.log b/integrations/dash-ariada/test-report/logs/install.log
new file mode 100644
index 00000000..f5d0698f
--- /dev/null
+++ b/integrations/dash-ariada/test-report/logs/install.log
@@ -0,0 +1,57 @@
+Obtaining file:///Users/pedro/adopta-s93-dash/integrations/dash-ariada
+ Installing build dependencies: started
+ Installing build dependencies: finished with status 'done'
+ Checking if build backend supports build_editable: started
+ Checking if build backend supports build_editable: finished with status 'done'
+ Getting requirements to build editable: started
+ Getting requirements to build editable: finished with status 'done'
+ Preparing editable metadata (pyproject.toml): started
+ Preparing editable metadata (pyproject.toml): finished with status 'done'
+Collecting build>=1.2 (from dash-ariada==0.1.0)
+ Using cached build-1.4.4-py3-none-any.whl.metadata (5.8 kB)
+Collecting pytest>=8.2 (from dash-ariada==0.1.0)
+ Using cached pytest-8.4.2-py3-none-any.whl.metadata (7.7 kB)
+Collecting ruff>=0.8 (from dash-ariada==0.1.0)
+ Using cached ruff-0.15.18-py3-none-macosx_11_0_arm64.whl.metadata (26 kB)
+Collecting packaging>=24.0 (from build>=1.2->dash-ariada==0.1.0)
+ Using cached packaging-26.2-py3-none-any.whl.metadata (3.5 kB)
+Collecting pyproject_hooks (from build>=1.2->dash-ariada==0.1.0)
+ Using cached pyproject_hooks-1.2.0-py3-none-any.whl.metadata (1.3 kB)
+Collecting importlib-metadata>=4.6 (from build>=1.2->dash-ariada==0.1.0)
+ Using cached importlib_metadata-8.7.1-py3-none-any.whl.metadata (4.7 kB)
+Collecting tomli>=1.1.0 (from build>=1.2->dash-ariada==0.1.0)
+ Using cached tomli-2.4.1-py3-none-any.whl.metadata (10 kB)
+Collecting zipp>=3.20 (from importlib-metadata>=4.6->build>=1.2->dash-ariada==0.1.0)
+ Using cached zipp-3.23.1-py3-none-any.whl.metadata (3.6 kB)
+Collecting exceptiongroup>=1 (from pytest>=8.2->dash-ariada==0.1.0)
+ Using cached exceptiongroup-1.3.1-py3-none-any.whl.metadata (6.7 kB)
+Collecting iniconfig>=1 (from pytest>=8.2->dash-ariada==0.1.0)
+ Using cached iniconfig-2.1.0-py3-none-any.whl.metadata (2.7 kB)
+Collecting pluggy<2,>=1.5 (from pytest>=8.2->dash-ariada==0.1.0)
+ Using cached pluggy-1.6.0-py3-none-any.whl.metadata (4.8 kB)
+Collecting pygments>=2.7.2 (from pytest>=8.2->dash-ariada==0.1.0)
+ Using cached pygments-2.20.0-py3-none-any.whl.metadata (2.5 kB)
+Collecting typing-extensions>=4.6.0 (from exceptiongroup>=1->pytest>=8.2->dash-ariada==0.1.0)
+ Using cached typing_extensions-4.15.0-py3-none-any.whl.metadata (3.3 kB)
+Using cached build-1.4.4-py3-none-any.whl (25 kB)
+Using cached importlib_metadata-8.7.1-py3-none-any.whl (27 kB)
+Using cached packaging-26.2-py3-none-any.whl (100 kB)
+Using cached pytest-8.4.2-py3-none-any.whl (365 kB)
+Using cached pluggy-1.6.0-py3-none-any.whl (20 kB)
+Using cached exceptiongroup-1.3.1-py3-none-any.whl (16 kB)
+Using cached iniconfig-2.1.0-py3-none-any.whl (6.0 kB)
+Using cached pygments-2.20.0-py3-none-any.whl (1.2 MB)
+Using cached ruff-0.15.18-py3-none-macosx_11_0_arm64.whl (10.6 MB)
+Using cached tomli-2.4.1-py3-none-any.whl (14 kB)
+Using cached typing_extensions-4.15.0-py3-none-any.whl (44 kB)
+Using cached zipp-3.23.1-py3-none-any.whl (10 kB)
+Using cached pyproject_hooks-1.2.0-py3-none-any.whl (10 kB)
+Building wheels for collected packages: dash-ariada
+ Building editable for dash-ariada (pyproject.toml): started
+ Building editable for dash-ariada (pyproject.toml): finished with status 'done'
+ Created wheel for dash-ariada: filename=dash_ariada-0.1.0-0.editable-py3-none-any.whl size=3793 sha256=b462c4c023c61feffa5d22dc8c1bacae9a07769ff904356c9e48b2bcfb4ac359
+ Stored in directory: /private/var/folders/2c/_42xj0l179z8yc7wmp5k91s00000gn/T/pip-ephem-wheel-cache-udndga51/wheels/a9/1b/d1/6e5196dbb361132852364ea23b3a58f672f5c78a3eb0eaffd5
+Successfully built dash-ariada
+Installing collected packages: zipp, typing-extensions, tomli, ruff, pyproject_hooks, pygments, pluggy, packaging, iniconfig, dash-ariada, importlib-metadata, exceptiongroup, pytest, build
+
+Successfully installed build-1.4.4 dash-ariada-0.1.0 exceptiongroup-1.3.1 importlib-metadata-8.7.1 iniconfig-2.1.0 packaging-26.2 pluggy-1.6.0 pygments-2.20.0 pyproject_hooks-1.2.0 pytest-8.4.2 ruff-0.15.18 tomli-2.4.1 typing-extensions-4.15.0 zipp-3.23.1
diff --git a/integrations/dash-ariada/test-report/logs/pip-upgrade.log b/integrations/dash-ariada/test-report/logs/pip-upgrade.log
new file mode 100644
index 00000000..4f3e9e08
--- /dev/null
+++ b/integrations/dash-ariada/test-report/logs/pip-upgrade.log
@@ -0,0 +1,9 @@
+Requirement already satisfied: pip in /private/tmp/ariada-dash-venv/lib/python3.9/site-packages (21.2.4)
+Collecting pip
+ Using cached pip-26.0.1-py3-none-any.whl (1.8 MB)
+Installing collected packages: pip
+ Attempting uninstall: pip
+ Found existing installation: pip 21.2.4
+ Uninstalling pip-21.2.4:
+ Successfully uninstalled pip-21.2.4
+Successfully installed pip-26.0.1
diff --git a/integrations/dash-ariada/test-report/logs/pytest.exit b/integrations/dash-ariada/test-report/logs/pytest.exit
new file mode 100644
index 00000000..c2270834
--- /dev/null
+++ b/integrations/dash-ariada/test-report/logs/pytest.exit
@@ -0,0 +1 @@
+0
\ No newline at end of file
diff --git a/integrations/dash-ariada/test-report/logs/pytest.log b/integrations/dash-ariada/test-report/logs/pytest.log
new file mode 100644
index 00000000..47d83522
--- /dev/null
+++ b/integrations/dash-ariada/test-report/logs/pytest.log
@@ -0,0 +1,2 @@
+..... [100%]
+5 passed in 0.02s
diff --git a/integrations/dash-ariada/test-report/logs/ruff.exit b/integrations/dash-ariada/test-report/logs/ruff.exit
new file mode 100644
index 00000000..c2270834
--- /dev/null
+++ b/integrations/dash-ariada/test-report/logs/ruff.exit
@@ -0,0 +1 @@
+0
\ No newline at end of file
diff --git a/integrations/dash-ariada/test-report/logs/ruff.log b/integrations/dash-ariada/test-report/logs/ruff.log
new file mode 100644
index 00000000..1f5f344d
--- /dev/null
+++ b/integrations/dash-ariada/test-report/logs/ruff.log
@@ -0,0 +1 @@
+All checks passed!
diff --git a/integrations/dash-ariada/test-report/logs/scan.exit b/integrations/dash-ariada/test-report/logs/scan.exit
new file mode 100644
index 00000000..c2270834
--- /dev/null
+++ b/integrations/dash-ariada/test-report/logs/scan.exit
@@ -0,0 +1 @@
+0
\ No newline at end of file
diff --git a/integrations/dash-ariada/test-report/logs/scan.log b/integrations/dash-ariada/test-report/logs/scan.log
new file mode 100644
index 00000000..dcaaf2f0
--- /dev/null
+++ b/integrations/dash-ariada/test-report/logs/scan.log
@@ -0,0 +1,2 @@
+http://127.0.0.1:8766/index.html: 12 finding(s), exit 0
+report: /Users/pedro/adopta-s93-dash/integrations/dash-ariada/scan-evidence/ariada-output/multi-domain-report.json
diff --git a/integrations/dash-ariada/test-report/logs/screenshot.log b/integrations/dash-ariada/test-report/logs/screenshot.log
new file mode 100644
index 00000000..e69de29b
diff --git a/integrations/dash-ariada/test-report/result.html b/integrations/dash-ariada/test-report/result.html
new file mode 100644
index 00000000..a186ba36
--- /dev/null
+++ b/integrations/dash-ariada/test-report/result.html
@@ -0,0 +1,209 @@
+
+
+
+
+
+Ariada Dash test report
+
+
+
+
Ariada Dash test report
+
Focused local gates for the Dash helper.
install
pass
pip install -e .[dev]
+
ruff
pass
ruff check .
+
pytest
pass
pytest -q
+
compileall
pass
python -m compileall -q dash_ariada tests
+
build
pass
python -m build
+
ariada-cli-build
pass
pnpm --filter @ariada-org/cli build
+
scan
pass
dash-ariada scan http://127.0.0.1:<fixture-port>
Logs
install log
Obtaining file:///Users/pedro/adopta-s93-dash/integrations/dash-ariada
+ Installing build dependencies: started
+ Installing build dependencies: finished with status 'done'
+ Checking if build backend supports build_editable: started
+ Checking if build backend supports build_editable: finished with status 'done'
+ Getting requirements to build editable: started
+ Getting requirements to build editable: finished with status 'done'
+ Preparing editable metadata (pyproject.toml): started
+ Preparing editable metadata (pyproject.toml): finished with status 'done'
+Collecting build>=1.2 (from dash-ariada==0.1.0)
+ Using cached build-1.4.4-py3-none-any.whl.metadata (5.8 kB)
+Collecting pytest>=8.2 (from dash-ariada==0.1.0)
+ Using cached pytest-8.4.2-py3-none-any.whl.metadata (7.7 kB)
+Collecting ruff>=0.8 (from dash-ariada==0.1.0)
+ Using cached ruff-0.15.18-py3-none-macosx_11_0_arm64.whl.metadata (26 kB)
+Collecting packaging>=24.0 (from build>=1.2->dash-ariada==0.1.0)
+ Using cached packaging-26.2-py3-none-any.whl.metadata (3.5 kB)
+Collecting pyproject_hooks (from build>=1.2->dash-ariada==0.1.0)
+ Using cached pyproject_hooks-1.2.0-py3-none-any.whl.metadata (1.3 kB)
+Collecting importlib-metadata>=4.6 (from build>=1.2->dash-ariada==0.1.0)
+ Using cached importlib_metadata-8.7.1-py3-none-any.whl.metadata (4.7 kB)
+Collecting tomli>=1.1.0 (from build>=1.2->dash-ariada==0.1.0)
+ Using cached tomli-2.4.1-py3-none-any.whl.metadata (10 kB)
+Collecting zipp>=3.20 (from importlib-metadata>=4.6->build>=1.2->dash-ariada==0.1.0)
+ Using cached zipp-3.23.1-py3-none-any.whl.metadata (3.6 kB)
+Collecting exceptiongroup>=1 (from pytest>=8.2->dash-ariada==0.1.0)
+ Using cached exceptiongroup-1.3.1-py3-none-any.whl.metadata (6.7 kB)
+Collecting iniconfig>=1 (from pytest>=8.2->dash-ariada==0.1.0)
+ Using cached iniconfig-2.1.0-py3-none-any.whl.metadata (2.7 kB)
+Collecting pluggy<2,>=1.5 (from pytest>=8.2->dash-ariada==0.1.0)
+ Using cached pluggy-1.6.0-py3-none-any.whl.metadata (4.8 kB)
+Collecting pygments>=2.7.2 (from pytest>=8.2->dash-ariada==0.1.0)
+ Using cached pygments-2.20.0-py3-none-any.whl.metadata (2.5 kB)
+Collecting typing-extensions>=4.6.0 (from exceptiongroup>=1->pytest>=8.2->dash-ariada==0.1.0)
+ Using cached typing_extensions-4.15.0-py3-none-any.whl.metadata (3.3 kB)
+Using cached build-1.4.4-py3-none-any.whl (25 kB)
+Using cached importlib_metadata-8.7.1-py3-none-any.whl (27 kB)
+Using cached packaging-26.2-py3-none-any.whl (100 kB)
+Using cached pytest-8.4.2-py3-none-any.whl (365 kB)
+Using cached pluggy-1.6.0-py3-none-any.whl (20 kB)
+Using cached exceptiongroup-1.3.1-py3-none-any.whl (16 kB)
+Using cached iniconfig-2.1.0-py3-none-any.whl (6.0 kB)
+Using cached pygments-2.20.0-py3-none-any.whl (1.2 MB)
+Using cached ruff-0.15.18-py3-none-macosx_11_0_arm64.whl (10.6 MB)
+Using cached tomli-2.4.1-py3-none-any.whl (14 kB)
+Using cached typing_extensions-4.15.0-py3-none-any.whl (44 kB)
+Using cached zipp-3.23.1-py3-none-any.whl (10 kB)
+Using cached pyproject_hooks-1.2.0-py3-none-any.whl (10 kB)
+Building wheels for collected packages: dash-ariada
+ Building editable for dash-ariada (pyproject.toml): started
+ Building editable for dash-ariada (pyproject.toml): finished with status 'done'
+ Created wheel for dash-ariada: filename=dash_ariada-0.1.0-0.editable-py3-none-any.whl size=3793 sha256=b462c4c023c61feffa5d22dc8c1bacae9a07769ff904356c9e48b2bcfb4ac359
+ Stored in directory: /private/var/folders/2c/_42xj0l179z8yc7wmp5k91s00000gn/T/pip-ephem-wheel-cache-udndga51/wheels/a9/1b/d1/6e5196dbb361132852364ea23b3a58f672f5c78a3eb0eaffd5
+Successfully built dash-ariada
+Installing collected packages: zipp, typing-extensions, tomli, ruff, pyproject_hooks, pygments, pluggy, packaging, iniconfig, dash-ariada, importlib-metadata, exceptiongroup, pytest, build
+
+Successfully installed build-1.4.4 dash-ariada-0.1.0 exceptiongroup-1.3.1 importlib-metadata-8.7.1 iniconfig-2.1.0 packaging-26.2 pluggy-1.6.0 pygments-2.20.0 pyproject_hooks-1.2.0 pytest-8.4.2 ruff-0.15.18 tomli-2.4.1 typing-extensions-4.15.0 zipp-3.23.1
PyPI publication and deployed-site scanning require founder-owned PyPI credentials
+and a deployed Django site. Local host-surface evidence is complete.
+
+
\ No newline at end of file
diff --git a/integrations/django-ariada/scan-evidence/scan-result-preview.html b/integrations/django-ariada/scan-evidence/scan-result-preview.html
new file mode 100644
index 00000000..1cf42ec5
--- /dev/null
+++ b/integrations/django-ariada/scan-evidence/scan-result-preview.html
@@ -0,0 +1,238 @@
+
+
+
+
+
+Ariada Django real scan preview
+
+
+
+
Ariada Django real scan preview
+
+
Real Ariada CLI scan triggered through python manage.py ariada_scan /broken/.
+
7 finding(s) in scan-evidence/ariada-output/multi-domain-report.json.
Representative host surface: a minimal Django project rendered through the Django
+test client.
+
Scanner path: Django management command to temporary localhost HTML to
+@ariada-org/cli.
+
{total} finding(s) were reported by the shared scanner CLI.
+{shot}
+
Command Output
+
{esc(read(SCAN_EVIDENCE / "command.log") or "(no command output)")}
+
Host Blockers
+
PyPI publication and deployed-site scanning require founder-owned PyPI credentials
+and a deployed Django site. Local host-surface evidence is complete.
Representative host surface: ASP.NET-style static publish output in examples/aspnet-static-output/wwwroot/index.html.
+
Scanner path: local fixture served over localhost to the shared @ariada-org/cli. The .NET wrapper is a thin adapter over the same CLI and does not reimplement scan rules.
+
4 finding(s) were reported by the shared scanner CLI.
+Browser screenshot of the real scan result preview.
+
dotnet is not installed on this host, so package build/test/pack/format are blocked until a .NET 8 SDK is installed. NuGet.org publication requires founder-owned NuGet credentials and API key.
+
+
\ No newline at end of file
diff --git a/integrations/dotnet-ariada/scan-evidence/scan-result-preview.html b/integrations/dotnet-ariada/scan-evidence/scan-result-preview.html
new file mode 100644
index 00000000..ebb65d17
--- /dev/null
+++ b/integrations/dotnet-ariada/scan-evidence/scan-result-preview.html
@@ -0,0 +1,161 @@
+
+
+
+
+
+Ariada .NET real scan preview
+
+
+
+
Ariada .NET real scan preview
+
+
Real Ariada CLI scan against a representative ASP.NET static publish output fixture.
+
4 finding(s) in scan-evidence/ariada-output/multi-domain-report.json.
Real Ariada CLI scan against a representative ASP.NET static publish output fixture.
+
{total} finding(s) in {esc(path.relative_to(ROOT))}.
+
Command Output
+
{esc(command or "(no command output)")}
+
Report Summary
+
{esc(json.dumps(report, indent=2)[:12000])}
+"""
+ SCAN_EVIDENCE.mkdir(parents=True, exist_ok=True)
+ (SCAN_EVIDENCE / "scan-result-preview.html").write_text(
+ page("Ariada .NET real scan preview", body),
+ encoding="utf-8",
+ )
+
+
+def build_scan_report() -> None:
+ path = report_path()
+ report = json.loads(read(path)) if path.exists() else {}
+ total = scan_total(report)
+ screenshot = SCAN_EVIDENCE / "screenshots" / "scan-result.png"
+ if screenshot.exists():
+ encoded = base64.b64encode(screenshot.read_bytes()).decode("ascii")
+ shot = (
+ ""
+ "Browser screenshot of the real scan result preview."
+ )
+ else:
+ shot = "
Evidence gap: screenshot file was not produced.
"
+ body = f"""
+
Representative host surface: ASP.NET-style static publish output in examples/aspnet-static-output/wwwroot/index.html.
+
Scanner path: local fixture served over localhost to the shared @ariada-org/cli. The .NET wrapper is a thin adapter over the same CLI and does not reimplement scan rules.
+
{total} finding(s) were reported by the shared scanner CLI.
+{shot}
+
Command Output
+
{esc(read(SCAN_EVIDENCE / "command.log") or "(no command output)")}
+
Host Blockers
+
dotnet is not installed on this host, so package build/test/pack/format are blocked until a .NET 8 SDK is installed. NuGet.org publication requires founder-owned NuGet credentials and API key.
+"""
+ (SCAN_EVIDENCE / "result.html").write_text(
+ page("Ariada .NET scan evidence", body),
+ encoding="utf-8",
+ )
+
+
+def main() -> None:
+ build_test_report()
+ build_scan_preview()
+ build_scan_report()
+
+
+if __name__ == "__main__":
+ main()
+
diff --git a/integrations/dotnet-ariada/scripts/capture_scan_screenshot.mjs b/integrations/dotnet-ariada/scripts/capture_scan_screenshot.mjs
new file mode 100644
index 00000000..6af86d6f
--- /dev/null
+++ b/integrations/dotnet-ariada/scripts/capture_scan_screenshot.mjs
@@ -0,0 +1,20 @@
+#!/usr/bin/env node
+import { createRequire } from 'node:module';
+import { mkdir } from 'node:fs/promises';
+import { dirname, resolve } from 'node:path';
+
+const require = createRequire(new URL('../../../packages/core-playwright/package.json', import.meta.url));
+const { chromium } = require('playwright');
+
+const [htmlPath, screenshotPath] = process.argv.slice(2);
+if (!htmlPath || !screenshotPath) {
+ console.error('Usage: node scripts/capture_scan_screenshot.mjs ');
+ process.exit(2);
+}
+
+await mkdir(dirname(resolve(screenshotPath)), { recursive: true });
+const browser = await chromium.launch({ headless: true });
+const page = await browser.newPage({ viewport: { width: 1280, height: 960 } });
+await page.goto(`file://${resolve(htmlPath)}`, { waitUntil: 'networkidle' });
+await page.screenshot({ path: screenshotPath, fullPage: true });
+await browser.close();
diff --git a/integrations/dotnet-ariada/scripts/validate-structure.mjs b/integrations/dotnet-ariada/scripts/validate-structure.mjs
new file mode 100644
index 00000000..d286fa12
--- /dev/null
+++ b/integrations/dotnet-ariada/scripts/validate-structure.mjs
@@ -0,0 +1,49 @@
+#!/usr/bin/env node
+import { access, readFile } from 'node:fs/promises';
+import { join } from 'node:path';
+
+const root = new URL('..', import.meta.url);
+const required = [
+ 'dotnet-ariada.sln',
+ 'README.md',
+ 'src/Ariada.DotNet.Core/Ariada.DotNet.Core.csproj',
+ 'src/Ariada.DotNet.Core/AriadaCliRunner.cs',
+ 'src/Ariada.DotNet.Core/AriadaReportParser.cs',
+ 'src/Ariada.DotNet.Tool/Ariada.DotNet.Tool.csproj',
+ 'src/Ariada.DotNet.Tool/Program.cs',
+ 'src/Ariada.DotNet.MSBuild/Ariada.DotNet.MSBuild.csproj',
+ 'src/Ariada.DotNet.MSBuild/AriadaScanTask.cs',
+ 'src/Ariada.DotNet.MSBuild/build/Ariada.DotNet.MSBuild.targets',
+ 'tests/Ariada.DotNet.Tests/AriadaReportParserTests.cs',
+ 'examples/aspnet-static-output/wwwroot/index.html',
+];
+
+const failures = [];
+for (const file of required) {
+ try {
+ await access(new URL(file, root));
+ } catch {
+ failures.push(`missing ${file}`);
+ }
+}
+
+const runner = await readFile(new URL('src/Ariada.DotNet.Core/AriadaCliRunner.cs', root), 'utf8');
+if (!runner.includes('"scan"') || !runner.includes('--output-dir')) {
+ failures.push('AriadaCliRunner must build ariada scan command arguments');
+}
+if (!runner.includes('ProcessStartInfo')) {
+ failures.push('AriadaCliRunner must invoke the shared CLI as a subprocess');
+}
+
+const msbuild = await readFile(new URL('src/Ariada.DotNet.MSBuild/AriadaScanTask.cs', root), 'utf8');
+if (!msbuild.includes('AriadaCliRunner') || !msbuild.includes('Log.LogError')) {
+ failures.push('MSBuild task must reuse core runner and fail the build on gate errors');
+}
+
+if (failures.length > 0) {
+ console.error(failures.join('\n'));
+ process.exit(1);
+}
+
+console.log(`PASS dotnet-ariada structure (${required.length} files)`);
+
diff --git a/integrations/dotnet-ariada/src/Ariada.DotNet.Core/Ariada.DotNet.Core.csproj b/integrations/dotnet-ariada/src/Ariada.DotNet.Core/Ariada.DotNet.Core.csproj
new file mode 100644
index 00000000..990ad767
--- /dev/null
+++ b/integrations/dotnet-ariada/src/Ariada.DotNet.Core/Ariada.DotNet.Core.csproj
@@ -0,0 +1,13 @@
+
+
+ net8.0
+ enable
+ enable
+ Ariada.DotNet.Core
+ 0.1.0
+ Alexander Brichkin (Agonist Development AB)
+ EUPL-1.2
+ Shared Ariada CLI invocation and gate parsing for .NET integrations.
+
+
+
diff --git a/integrations/dotnet-ariada/src/Ariada.DotNet.Core/AriadaCliRunner.cs b/integrations/dotnet-ariada/src/Ariada.DotNet.Core/AriadaCliRunner.cs
new file mode 100644
index 00000000..baea3c1d
--- /dev/null
+++ b/integrations/dotnet-ariada/src/Ariada.DotNet.Core/AriadaCliRunner.cs
@@ -0,0 +1,224 @@
+// SPDX-FileCopyrightText: 2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+
+using System.Diagnostics;
+using System.Net;
+using System.Net.Sockets;
+
+namespace Ariada.DotNet.Core;
+
+public interface IAriadaProcessRunner
+{
+ Task RunAsync(IReadOnlyList command, CancellationToken cancellationToken);
+}
+
+public sealed record ProcessResult(int ExitCode, string StandardOutput, string StandardError);
+
+public sealed class SystemProcessRunner : IAriadaProcessRunner
+{
+ public async Task RunAsync(IReadOnlyList command, CancellationToken cancellationToken)
+ {
+ if (command.Count == 0)
+ {
+ throw new ArgumentException("Command must not be empty.", nameof(command));
+ }
+
+ var start = new ProcessStartInfo
+ {
+ FileName = command[0],
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ };
+
+ foreach (var arg in command.Skip(1))
+ {
+ start.ArgumentList.Add(arg);
+ }
+
+ using var process = Process.Start(start) ?? throw new InvalidOperationException("Could not start Ariada CLI.");
+ var stdout = process.StandardOutput.ReadToEndAsync(cancellationToken);
+ var stderr = process.StandardError.ReadToEndAsync(cancellationToken);
+ await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
+ return new ProcessResult(process.ExitCode, await stdout.ConfigureAwait(false), await stderr.ConfigureAwait(false));
+ }
+}
+
+public sealed class AriadaCliRunner
+{
+ private readonly IAriadaProcessRunner processRunner;
+
+ public AriadaCliRunner(IAriadaProcessRunner? processRunner = null)
+ {
+ this.processRunner = processRunner ?? new SystemProcessRunner();
+ }
+
+ public async Task RunAsync(AriadaOptions options, CancellationToken cancellationToken = default)
+ {
+ Directory.CreateDirectory(options.OutputDirectory);
+ await using var served = LocalStaticTarget.TryServe(options.Target, cancellationToken);
+ var command = BuildCommand(options with { Target = served.Url });
+ var completed = await processRunner.RunAsync(command, cancellationToken).ConfigureAwait(false);
+ var reportPath = FindReport(options.OutputDirectory);
+ var findings = reportPath is null ? Array.Empty() : AriadaReportParser.ParseReportFile(reportPath);
+
+ return new AriadaScanResult(
+ options.Target,
+ completed.ExitCode,
+ completed.StandardOutput,
+ completed.StandardError,
+ reportPath,
+ findings);
+ }
+
+ public static IReadOnlyList BuildCommand(AriadaOptions options)
+ {
+ var command = new List
+ {
+ options.CliCommand,
+ "scan",
+ options.Target,
+ "--format",
+ options.Format,
+ "--output-dir",
+ options.OutputDirectory,
+ "--browser",
+ options.Browser,
+ "--severity-threshold",
+ options.SeverityThreshold,
+ "--timeout-ms",
+ options.TimeoutMilliseconds.ToString(),
+ };
+
+ if (options.Domains is { Count: > 0 })
+ {
+ command.Add("--domains");
+ command.Add(string.Join(",", options.Domains));
+ }
+
+ return command;
+ }
+
+ private static string? FindReport(string outputDirectory)
+ {
+ var multi = Path.Combine(outputDirectory, "multi-domain-report.json");
+ if (File.Exists(multi))
+ {
+ return multi;
+ }
+
+ var single = Path.Combine(outputDirectory, "scan.json");
+ return File.Exists(single) ? single : null;
+ }
+}
+
+internal sealed class ServedTarget : IAsyncDisposable
+{
+ private readonly HttpListener? listener;
+ private readonly Task? serverTask;
+
+ public ServedTarget(string url, HttpListener? listener = null, Task? serverTask = null)
+ {
+ Url = url;
+ this.listener = listener;
+ this.serverTask = serverTask;
+ }
+
+ public string Url { get; }
+
+ public async ValueTask DisposeAsync()
+ {
+ if (listener is null)
+ {
+ return;
+ }
+
+ listener.Stop();
+ listener.Close();
+ if (serverTask is not null)
+ {
+ try
+ {
+ await serverTask.ConfigureAwait(false);
+ }
+ catch (HttpListenerException)
+ {
+ }
+ catch (ObjectDisposedException)
+ {
+ }
+ }
+ }
+}
+
+internal static class LocalStaticTarget
+{
+ public static ServedTarget TryServe(string target, CancellationToken cancellationToken)
+ {
+ if (!Directory.Exists(target))
+ {
+ return new ServedTarget(target);
+ }
+
+ var root = Path.GetFullPath(target);
+ var port = ReserveLoopbackPort();
+ var url = $"http://127.0.0.1:{port}/";
+ var listener = new HttpListener();
+ listener.Prefixes.Add(url);
+ listener.Start();
+ var task = Task.Run(() => ServeAsync(listener, root, cancellationToken), cancellationToken);
+ return new ServedTarget(url, listener, task);
+ }
+
+ private static async Task ServeAsync(HttpListener listener, string root, CancellationToken cancellationToken)
+ {
+ while (listener.IsListening && !cancellationToken.IsCancellationRequested)
+ {
+ var context = await listener.GetContextAsync().ConfigureAwait(false);
+ _ = Task.Run(() => RespondAsync(context, root), cancellationToken);
+ }
+ }
+
+ private static async Task RespondAsync(HttpListenerContext context, string root)
+ {
+ var raw = context.Request.Url?.AbsolutePath.TrimStart('/') ?? "";
+ var relative = string.IsNullOrWhiteSpace(raw) ? "index.html" : Uri.UnescapeDataString(raw);
+ var candidate = Path.GetFullPath(Path.Combine(root, relative));
+ if (!candidate.StartsWith(root, StringComparison.Ordinal) || !File.Exists(candidate))
+ {
+ context.Response.StatusCode = 404;
+ context.Response.Close();
+ return;
+ }
+
+ var bytes = await File.ReadAllBytesAsync(candidate).ConfigureAwait(false);
+ context.Response.ContentType = ContentTypeFor(candidate);
+ context.Response.ContentLength64 = bytes.Length;
+ await context.Response.OutputStream.WriteAsync(bytes).ConfigureAwait(false);
+ context.Response.Close();
+ }
+
+ private static int ReserveLoopbackPort()
+ {
+ var listener = new TcpListener(IPAddress.Loopback, 0);
+ listener.Start();
+ var port = ((IPEndPoint)listener.LocalEndpoint).Port;
+ listener.Stop();
+ return port;
+ }
+
+ private static string ContentTypeFor(string path)
+ {
+ return Path.GetExtension(path).ToLowerInvariant() switch
+ {
+ ".html" => "text/html; charset=utf-8",
+ ".css" => "text/css; charset=utf-8",
+ ".js" => "text/javascript; charset=utf-8",
+ ".json" => "application/json; charset=utf-8",
+ ".png" => "image/png",
+ ".jpg" or ".jpeg" => "image/jpeg",
+ ".svg" => "image/svg+xml",
+ _ => "application/octet-stream",
+ };
+ }
+}
diff --git a/integrations/dotnet-ariada/src/Ariada.DotNet.Core/AriadaOptions.cs b/integrations/dotnet-ariada/src/Ariada.DotNet.Core/AriadaOptions.cs
new file mode 100644
index 00000000..0caa8380
--- /dev/null
+++ b/integrations/dotnet-ariada/src/Ariada.DotNet.Core/AriadaOptions.cs
@@ -0,0 +1,29 @@
+// SPDX-FileCopyrightText: 2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+
+namespace Ariada.DotNet.Core;
+
+public sealed record AriadaOptions(
+ string Target,
+ string OutputDirectory,
+ string CliCommand = "ariada",
+ string Browser = "chromium",
+ string Format = "json",
+ string SeverityThreshold = "moderate",
+ int TimeoutMilliseconds = 30000,
+ IReadOnlyList? Domains = null);
+
+public sealed record AriadaFinding(string RuleId, string Severity, string Domain);
+
+public sealed record AriadaScanResult(
+ string Target,
+ int ExitCode,
+ string StandardOutput,
+ string StandardError,
+ string? ReportPath,
+ IReadOnlyList Findings)
+{
+ public bool GateFailed => ExitCode == 1;
+
+ public bool RuntimeFailed => ExitCode >= 2;
+}
diff --git a/integrations/dotnet-ariada/src/Ariada.DotNet.Core/AriadaReportParser.cs b/integrations/dotnet-ariada/src/Ariada.DotNet.Core/AriadaReportParser.cs
new file mode 100644
index 00000000..9caca83a
--- /dev/null
+++ b/integrations/dotnet-ariada/src/Ariada.DotNet.Core/AriadaReportParser.cs
@@ -0,0 +1,101 @@
+// SPDX-FileCopyrightText: 2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+
+using System.Text.Json;
+
+namespace Ariada.DotNet.Core;
+
+public static class AriadaReportParser
+{
+ public static IReadOnlyList ParseFindings(string json)
+ {
+ using var document = JsonDocument.Parse(json);
+ var root = document.RootElement;
+ var findings = new List();
+
+ if (root.TryGetProperty("grid", out var grid) && grid.ValueKind == JsonValueKind.Object)
+ {
+ foreach (var site in grid.EnumerateObject())
+ {
+ if (site.Value.ValueKind != JsonValueKind.Object)
+ {
+ continue;
+ }
+
+ foreach (var domain in site.Value.EnumerateObject())
+ {
+ if (domain.Value.ValueKind != JsonValueKind.Array)
+ {
+ continue;
+ }
+
+ foreach (var finding in domain.Value.EnumerateArray())
+ {
+ findings.Add(ReadFinding(finding, domain.Name));
+ }
+ }
+ }
+ }
+
+ if (root.TryGetProperty("report", out var report) &&
+ report.ValueKind == JsonValueKind.Object &&
+ report.TryGetProperty("findings", out var singleFindings))
+ {
+ ReadSingleScanFindings(singleFindings, findings);
+ }
+
+ return findings;
+ }
+
+ public static IReadOnlyList ParseReportFile(string reportPath)
+ {
+ return File.Exists(reportPath)
+ ? ParseFindings(File.ReadAllText(reportPath))
+ : Array.Empty();
+ }
+
+ private static void ReadSingleScanFindings(JsonElement value, List findings)
+ {
+ if (value.ValueKind == JsonValueKind.Array)
+ {
+ foreach (var item in value.EnumerateArray())
+ {
+ findings.Add(ReadFinding(item, "accessibility"));
+ }
+ }
+
+ if (value.ValueKind == JsonValueKind.Object)
+ {
+ foreach (var group in value.EnumerateObject())
+ {
+ if (group.Value.ValueKind != JsonValueKind.Array)
+ {
+ continue;
+ }
+
+ foreach (var item in group.Value.EnumerateArray())
+ {
+ findings.Add(ReadFinding(item, group.Name));
+ }
+ }
+ }
+ }
+
+ private static AriadaFinding ReadFinding(JsonElement finding, string domain)
+ {
+ return new AriadaFinding(
+ ReadString(finding, "ruleId", "rule"),
+ ReadString(finding, "severity", "moderate"),
+ domain);
+ }
+
+ private static string ReadString(JsonElement element, string name, string fallback)
+ {
+ return element.ValueKind == JsonValueKind.Object &&
+ element.TryGetProperty(name, out var value) &&
+ value.ValueKind == JsonValueKind.String
+ ? value.GetString() ?? fallback
+ : fallback;
+ }
+}
+
diff --git a/integrations/dotnet-ariada/src/Ariada.DotNet.Core/AriadaSeverity.cs b/integrations/dotnet-ariada/src/Ariada.DotNet.Core/AriadaSeverity.cs
new file mode 100644
index 00000000..9edb90ed
--- /dev/null
+++ b/integrations/dotnet-ariada/src/Ariada.DotNet.Core/AriadaSeverity.cs
@@ -0,0 +1,26 @@
+// SPDX-FileCopyrightText: 2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+
+namespace Ariada.DotNet.Core;
+
+public static class AriadaSeverity
+{
+ private static readonly IReadOnlyDictionary Rank = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["minor"] = 1,
+ ["moderate"] = 2,
+ ["serious"] = 3,
+ ["critical"] = 4,
+ };
+
+ public static bool IsAtOrAbove(string severity, string threshold)
+ {
+ return ValueOf(severity) >= ValueOf(threshold);
+ }
+
+ public static int ValueOf(string severity)
+ {
+ return Rank.TryGetValue(severity, out var value) ? value : Rank["moderate"];
+ }
+}
+
diff --git a/integrations/dotnet-ariada/src/Ariada.DotNet.MSBuild/Ariada.DotNet.MSBuild.csproj b/integrations/dotnet-ariada/src/Ariada.DotNet.MSBuild/Ariada.DotNet.MSBuild.csproj
new file mode 100644
index 00000000..bb7a58da
--- /dev/null
+++ b/integrations/dotnet-ariada/src/Ariada.DotNet.MSBuild/Ariada.DotNet.MSBuild.csproj
@@ -0,0 +1,21 @@
+
+
+ net8.0
+ enable
+ enable
+ Ariada.DotNet.MSBuild
+ 0.1.0
+ Alexander Brichkin (Agonist Development AB)
+ EUPL-1.2
+ MSBuild task and target for running Ariada CLI scans in .NET builds.
+ false
+
+
+
+
+
+
+
+
+
+
diff --git a/integrations/dotnet-ariada/src/Ariada.DotNet.MSBuild/AriadaScanTask.cs b/integrations/dotnet-ariada/src/Ariada.DotNet.MSBuild/AriadaScanTask.cs
new file mode 100644
index 00000000..ee80b596
--- /dev/null
+++ b/integrations/dotnet-ariada/src/Ariada.DotNet.MSBuild/AriadaScanTask.cs
@@ -0,0 +1,70 @@
+// SPDX-FileCopyrightText: 2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+
+using Ariada.DotNet.Core;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Ariada.DotNet.MSBuild;
+
+public sealed class AriadaScanTask : Task
+{
+ [Required]
+ public string Target { get; set; } = "";
+
+ public string OutputDirectory { get; set; } = "obj/ariada-output";
+
+ public string CliCommand { get; set; } = "ariada";
+
+ public string Browser { get; set; } = "chromium";
+
+ public string SeverityThreshold { get; set; } = "moderate";
+
+ public string Domains { get; set; } = "";
+
+ public int TimeoutMilliseconds { get; set; } = 30000;
+
+ public override bool Execute()
+ {
+ try
+ {
+ var options = new AriadaOptions(
+ Target,
+ OutputDirectory,
+ CliCommand,
+ Browser,
+ "json",
+ SeverityThreshold,
+ TimeoutMilliseconds,
+ SplitDomains(Domains));
+
+ var result = new AriadaCliRunner().RunAsync(options).GetAwaiter().GetResult();
+ Log.LogMessage(MessageImportance.High, result.StandardOutput);
+ if (!string.IsNullOrWhiteSpace(result.StandardError))
+ {
+ Log.LogWarning(result.StandardError);
+ }
+
+ if (result.GateFailed)
+ {
+ Log.LogError($"Ariada scan failed the gate with {result.Findings.Count} finding(s). Report: {result.ReportPath ?? "not written"}");
+ return false;
+ }
+
+ return !result.RuntimeFailed;
+ }
+ catch (Exception ex)
+ {
+ Log.LogErrorFromException(ex, showStackTrace: true);
+ return false;
+ }
+ }
+
+ private static IReadOnlyList SplitDomains(string domains)
+ {
+ return string.IsNullOrWhiteSpace(domains)
+ ? Array.Empty()
+ : domains.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ }
+}
+
diff --git a/integrations/dotnet-ariada/src/Ariada.DotNet.MSBuild/build/Ariada.DotNet.MSBuild.targets b/integrations/dotnet-ariada/src/Ariada.DotNet.MSBuild/build/Ariada.DotNet.MSBuild.targets
new file mode 100644
index 00000000..03c1f864
--- /dev/null
+++ b/integrations/dotnet-ariada/src/Ariada.DotNet.MSBuild/build/Ariada.DotNet.MSBuild.targets
@@ -0,0 +1,22 @@
+
+
+
+
+ false
+ ariada
+ chromium
+ $(PublishDir)wwwroot
+ moderate
+ $(BaseIntermediateOutputPath)ariada-output
+
+
+
+
+
+
diff --git a/integrations/dotnet-ariada/src/Ariada.DotNet.Tool/Ariada.DotNet.Tool.csproj b/integrations/dotnet-ariada/src/Ariada.DotNet.Tool/Ariada.DotNet.Tool.csproj
new file mode 100644
index 00000000..b1a6726a
--- /dev/null
+++ b/integrations/dotnet-ariada/src/Ariada.DotNet.Tool/Ariada.DotNet.Tool.csproj
@@ -0,0 +1,19 @@
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ true
+ dotnet-ariada
+ Ariada.DotNet.Tool
+ 0.1.0
+ Alexander Brichkin (Agonist Development AB)
+ EUPL-1.2
+ .NET global tool wrapper for @ariada-org/cli accessibility scans.
+
+
+
+
+
+
diff --git a/integrations/dotnet-ariada/src/Ariada.DotNet.Tool/Program.cs b/integrations/dotnet-ariada/src/Ariada.DotNet.Tool/Program.cs
new file mode 100644
index 00000000..91199246
--- /dev/null
+++ b/integrations/dotnet-ariada/src/Ariada.DotNet.Tool/Program.cs
@@ -0,0 +1,111 @@
+// SPDX-FileCopyrightText: 2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+
+using Ariada.DotNet.Core;
+
+return await DotNetAriadaProgram.RunAsync(args).ConfigureAwait(false);
+
+internal static class DotNetAriadaProgram
+{
+ public static async Task RunAsync(string[] args)
+ {
+ if (args.Length == 0 || args[0] is "-h" or "--help")
+ {
+ PrintHelp();
+ return 0;
+ }
+
+ if (!string.Equals(args[0], "scan", StringComparison.OrdinalIgnoreCase))
+ {
+ Console.Error.WriteLine("Unknown command. Expected: scan");
+ return 2;
+ }
+
+ var parsed = ParseScanArgs(args.Skip(1).ToArray());
+ if (parsed is null)
+ {
+ return 2;
+ }
+
+ var result = await new AriadaCliRunner().RunAsync(parsed).ConfigureAwait(false);
+ Console.Write(result.StandardOutput);
+ if (!string.IsNullOrWhiteSpace(result.StandardError))
+ {
+ Console.Error.Write(result.StandardError);
+ }
+
+ Console.WriteLine($"Ariada .NET scan: {result.Findings.Count} finding(s), report: {result.ReportPath ?? "not written"}");
+ return result.ExitCode;
+ }
+
+ private static AriadaOptions? ParseScanArgs(string[] args)
+ {
+ if (args.Length == 0)
+ {
+ Console.Error.WriteLine("Missing scan target.");
+ return null;
+ }
+
+ var target = args[0];
+ var output = "ariada-output";
+ var cli = "ariada";
+ var browser = "chromium";
+ var threshold = "moderate";
+ var timeout = 30000;
+ IReadOnlyList? domains = null;
+
+ for (var i = 1; i < args.Length; i++)
+ {
+ var arg = args[i];
+ if (arg is "--output-dir" or "-o" && TryReadValue(args, ref i, out var outputValue))
+ {
+ output = outputValue;
+ }
+ else if (arg == "--cli" && TryReadValue(args, ref i, out var cliValue))
+ {
+ cli = cliValue;
+ }
+ else if (arg == "--browser" && TryReadValue(args, ref i, out var browserValue))
+ {
+ browser = browserValue;
+ }
+ else if (arg == "--threshold" && TryReadValue(args, ref i, out var thresholdValue))
+ {
+ threshold = thresholdValue;
+ }
+ else if (arg == "--timeout-ms" && TryReadValue(args, ref i, out var timeoutValue) && int.TryParse(timeoutValue, out var parsedTimeout))
+ {
+ timeout = parsedTimeout;
+ }
+ else if (arg == "--domains" && TryReadValue(args, ref i, out var domainsValue))
+ {
+ domains = domainsValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ }
+ else
+ {
+ Console.Error.WriteLine($"Unknown or incomplete option: {arg}");
+ return null;
+ }
+ }
+
+ return new AriadaOptions(target, output, cli, browser, "json", threshold, timeout, domains);
+ }
+
+ private static bool TryReadValue(string[] args, ref int index, out string value)
+ {
+ if (index + 1 >= args.Length)
+ {
+ value = "";
+ return false;
+ }
+
+ index++;
+ value = args[index];
+ return true;
+ }
+
+ private static void PrintHelp()
+ {
+ Console.WriteLine("dotnet-ariada scan [--threshold serious] [--domains accessibility,security]");
+ }
+}
diff --git a/integrations/dotnet-ariada/test-report/logs/dotnet-build.exit b/integrations/dotnet-ariada/test-report/logs/dotnet-build.exit
new file mode 100644
index 00000000..405e0570
--- /dev/null
+++ b/integrations/dotnet-ariada/test-report/logs/dotnet-build.exit
@@ -0,0 +1 @@
+127
\ No newline at end of file
diff --git a/integrations/dotnet-ariada/test-report/logs/dotnet-build.log b/integrations/dotnet-ariada/test-report/logs/dotnet-build.log
new file mode 100644
index 00000000..e397d651
--- /dev/null
+++ b/integrations/dotnet-ariada/test-report/logs/dotnet-build.log
@@ -0,0 +1 @@
+sh: dotnet: command not found
diff --git a/integrations/dotnet-ariada/test-report/logs/dotnet-format.exit b/integrations/dotnet-ariada/test-report/logs/dotnet-format.exit
new file mode 100644
index 00000000..405e0570
--- /dev/null
+++ b/integrations/dotnet-ariada/test-report/logs/dotnet-format.exit
@@ -0,0 +1 @@
+127
\ No newline at end of file
diff --git a/integrations/dotnet-ariada/test-report/logs/dotnet-format.log b/integrations/dotnet-ariada/test-report/logs/dotnet-format.log
new file mode 100644
index 00000000..e397d651
--- /dev/null
+++ b/integrations/dotnet-ariada/test-report/logs/dotnet-format.log
@@ -0,0 +1 @@
+sh: dotnet: command not found
diff --git a/integrations/dotnet-ariada/test-report/logs/dotnet-info.exit b/integrations/dotnet-ariada/test-report/logs/dotnet-info.exit
new file mode 100644
index 00000000..405e0570
--- /dev/null
+++ b/integrations/dotnet-ariada/test-report/logs/dotnet-info.exit
@@ -0,0 +1 @@
+127
\ No newline at end of file
diff --git a/integrations/dotnet-ariada/test-report/logs/dotnet-info.log b/integrations/dotnet-ariada/test-report/logs/dotnet-info.log
new file mode 100644
index 00000000..e397d651
--- /dev/null
+++ b/integrations/dotnet-ariada/test-report/logs/dotnet-info.log
@@ -0,0 +1 @@
+sh: dotnet: command not found
diff --git a/integrations/dotnet-ariada/test-report/logs/dotnet-pack.exit b/integrations/dotnet-ariada/test-report/logs/dotnet-pack.exit
new file mode 100644
index 00000000..405e0570
--- /dev/null
+++ b/integrations/dotnet-ariada/test-report/logs/dotnet-pack.exit
@@ -0,0 +1 @@
+127
\ No newline at end of file
diff --git a/integrations/dotnet-ariada/test-report/logs/dotnet-pack.log b/integrations/dotnet-ariada/test-report/logs/dotnet-pack.log
new file mode 100644
index 00000000..e397d651
--- /dev/null
+++ b/integrations/dotnet-ariada/test-report/logs/dotnet-pack.log
@@ -0,0 +1 @@
+sh: dotnet: command not found
diff --git a/integrations/dotnet-ariada/test-report/logs/dotnet-test.exit b/integrations/dotnet-ariada/test-report/logs/dotnet-test.exit
new file mode 100644
index 00000000..405e0570
--- /dev/null
+++ b/integrations/dotnet-ariada/test-report/logs/dotnet-test.exit
@@ -0,0 +1 @@
+127
\ No newline at end of file
diff --git a/integrations/dotnet-ariada/test-report/logs/dotnet-test.log b/integrations/dotnet-ariada/test-report/logs/dotnet-test.log
new file mode 100644
index 00000000..e397d651
--- /dev/null
+++ b/integrations/dotnet-ariada/test-report/logs/dotnet-test.log
@@ -0,0 +1 @@
+sh: dotnet: command not found
diff --git a/integrations/dotnet-ariada/test-report/logs/validate.exit b/integrations/dotnet-ariada/test-report/logs/validate.exit
new file mode 100644
index 00000000..c2270834
--- /dev/null
+++ b/integrations/dotnet-ariada/test-report/logs/validate.exit
@@ -0,0 +1 @@
+0
\ No newline at end of file
diff --git a/integrations/dotnet-ariada/test-report/logs/validate.log b/integrations/dotnet-ariada/test-report/logs/validate.log
new file mode 100644
index 00000000..1889a263
--- /dev/null
+++ b/integrations/dotnet-ariada/test-report/logs/validate.log
@@ -0,0 +1 @@
+PASS dotnet-ariada structure (12 files)
diff --git a/integrations/dotnet-ariada/test-report/result.html b/integrations/dotnet-ariada/test-report/result.html
new file mode 100644
index 00000000..032e2950
--- /dev/null
+++ b/integrations/dotnet-ariada/test-report/result.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+Ariada .NET test report
+
+
+
+
Ariada .NET test report
+
+
Focused local gates for the S102 .NET global tool and MSBuild task.
+
Host blocker: this Codex host has no dotnet executable, so .NET build/test/pack/format are blocked here. The structural gate is runnable and passed.
+
Gate
Result
Command
validate
pass
node scripts/validate-structure.mjs
+
dotnet-info
blocked
dotnet --info
+
dotnet-build
blocked
dotnet build -c Release
+
dotnet-test
blocked
dotnet test -c Release
+
dotnet-pack
blocked
dotnet pack -c Release
+
dotnet-format
blocked
dotnet format --verify-no-changes
+
Logs
+validate log
PASS dotnet-ariada structure (12 files)
+dotnet-info log
sh: dotnet: command not found
+dotnet-build log
sh: dotnet: command not found
+dotnet-test log
sh: dotnet: command not found
+dotnet-pack log
sh: dotnet: command not found
+dotnet-format log
sh: dotnet: command not found
+
+
\ No newline at end of file
diff --git a/integrations/dotnet-ariada/tests/Ariada.DotNet.Tests/Ariada.DotNet.Tests.csproj b/integrations/dotnet-ariada/tests/Ariada.DotNet.Tests/Ariada.DotNet.Tests.csproj
new file mode 100644
index 00000000..20a96cd2
--- /dev/null
+++ b/integrations/dotnet-ariada/tests/Ariada.DotNet.Tests/Ariada.DotNet.Tests.csproj
@@ -0,0 +1,15 @@
+
+
+ net8.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
diff --git a/integrations/dotnet-ariada/tests/Ariada.DotNet.Tests/AriadaReportParserTests.cs b/integrations/dotnet-ariada/tests/Ariada.DotNet.Tests/AriadaReportParserTests.cs
new file mode 100644
index 00000000..a9ad1cfc
--- /dev/null
+++ b/integrations/dotnet-ariada/tests/Ariada.DotNet.Tests/AriadaReportParserTests.cs
@@ -0,0 +1,134 @@
+// SPDX-FileCopyrightText: 2026 Agonist Development AB
+// SPDX-License-Identifier: EUPL-1.2
+
+using Ariada.DotNet.Core;
+using Xunit;
+
+namespace Ariada.DotNet.Tests;
+
+public sealed class AriadaReportParserTests
+{
+ [Fact]
+ public void ParsesMultiDomainGridFindings()
+ {
+ var findings = AriadaReportParser.ParseFindings(
+ """
+ {
+ "sites": ["http://example.test/"],
+ "domains": ["accessibility"],
+ "grid": {
+ "http://example.test/": {
+ "accessibility": [
+ {"ruleId": "image-alt", "severity": "critical"},
+ {"ruleId": "button-name", "severity": "serious"}
+ ]
+ }
+ }
+ }
+ """);
+
+ Assert.Equal(2, findings.Count);
+ Assert.Contains(findings, f => f.RuleId == "image-alt" && f.Severity == "critical");
+ }
+
+ [Fact]
+ public void BuildsCliCommandWithDomains()
+ {
+ var command = AriadaCliRunner.BuildCommand(new AriadaOptions(
+ "http://example.test/",
+ "out",
+ "ariada",
+ "chromium",
+ "json",
+ "serious",
+ 15000,
+ new[] { "accessibility", "security" }));
+
+ Assert.Equal("ariada", command[0]);
+ Assert.Contains("--domains", command);
+ Assert.Contains("accessibility,security", command);
+ }
+
+ [Fact]
+ public async Task RunnerReportsGateFailureFromStubbedCli()
+ {
+ using var temp = new TempDirectory();
+ var runner = new AriadaCliRunner(new StubProcessRunner(temp.Path));
+
+ var result = await runner.RunAsync(new AriadaOptions("http://example.test/", temp.Path));
+
+ Assert.True(result.GateFailed);
+ Assert.Equal(1, result.ExitCode);
+ Assert.Single(result.Findings);
+ }
+
+ [Fact]
+ public async Task RunnerServesStaticOutputDirectoryBeforeCallingCli()
+ {
+ using var staticRoot = new TempDirectory();
+ using var output = new TempDirectory();
+ File.WriteAllText(System.IO.Path.Combine(staticRoot.Path, "index.html"), "");
+ var runner = new AriadaCliRunner(new CapturingProcessRunner(output.Path));
+
+ var result = await runner.RunAsync(new AriadaOptions(staticRoot.Path, output.Path));
+
+ Assert.Equal(staticRoot.Path, result.Target);
+ }
+
+ private sealed class CapturingProcessRunner : IAriadaProcessRunner
+ {
+ private readonly string outputDirectory;
+
+ public CapturingProcessRunner(string outputDirectory)
+ {
+ this.outputDirectory = outputDirectory;
+ }
+
+ public Task RunAsync(IReadOnlyList command, CancellationToken cancellationToken)
+ {
+ Assert.StartsWith("http://127.0.0.1:", command[2]);
+ File.WriteAllText(
+ System.IO.Path.Combine(outputDirectory, "multi-domain-report.json"),
+ """
+ {"grid":{"http://127.0.0.1/":{"accessibility":[]}}}
+ """);
+ return Task.FromResult(new ProcessResult(0, "Wrote report\n", ""));
+ }
+ }
+
+ private sealed class StubProcessRunner : IAriadaProcessRunner
+ {
+ private readonly string outputDirectory;
+
+ public StubProcessRunner(string outputDirectory)
+ {
+ this.outputDirectory = outputDirectory;
+ }
+
+ public Task RunAsync(IReadOnlyList command, CancellationToken cancellationToken)
+ {
+ File.WriteAllText(
+ Path.Combine(outputDirectory, "multi-domain-report.json"),
+ """
+ {"grid":{"http://example.test/":{"accessibility":[{"ruleId":"image-alt","severity":"critical"}]}}}
+ """);
+ return Task.FromResult(new ProcessResult(1, "Wrote report\n", ""));
+ }
+ }
+
+ private sealed class TempDirectory : IDisposable
+ {
+ public TempDirectory()
+ {
+ Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"ariada-dotnet-{Guid.NewGuid():N}");
+ Directory.CreateDirectory(Path);
+ }
+
+ public string Path { get; }
+
+ public void Dispose()
+ {
+ Directory.Delete(Path, recursive: true);
+ }
+ }
+}
diff --git a/integrations/drupal-ariada/README.md b/integrations/drupal-ariada/README.md
new file mode 100644
index 00000000..17f92fcc
--- /dev/null
+++ b/integrations/drupal-ariada/README.md
@@ -0,0 +1,97 @@
+
+
+# Ariada Drupal Module
+
+Drupal 10/11 module that runs Ariada accessibility scans from the admin UI and
+from Drush. The module is a thin adapter: it does not implement scanning logic.
+It calls the Ariada CLI locally, or an explicitly configured hosted scan
+endpoint.
+
+## Install
+
+Place this directory at `web/modules/custom/ariada_drupal` in a Drupal site, or
+install it as a Composer path repository during development:
+
+```json
+{
+ "repositories": [
+ {
+ "type": "path",
+ "url": "../integrations/drupal-ariada"
+ }
+ ],
+ "require": {
+ "ariada/drupal-ariada": "*"
+ }
+}
+```
+
+Enable the module:
+
+```bash
+drush en ariada_drupal -y
+drush cache:rebuild
+```
+
+## Local CLI Mode
+
+Install the Ariada CLI where PHP can execute it:
+
+```bash
+npm install -g @ariada-org/cli
+npx playwright install chromium
+ariada version
+```
+
+Configure the Drupal module at
+`/admin/config/development/ariada`. In local mode, the scanner runs:
+
+```bash
+ariada scan --format json --output-dir --severity-threshold --timeout-ms
+```
+
+The module accepts CLI exit code `0` for a clean scan and `1` for findings, then
+reads `scan.json` from the output directory.
+
+## Hosted Endpoint Mode
+
+Hosted mode is opt-in. Configure an endpoint base URL and API key in the admin
+form. The module calls `POST /api/scan` with the URL and severity threshold. If
+the endpoint returns an asynchronous scan ID, the module polls
+`GET /api/scan/{id}` until it receives a completed report or times out.
+
+## Admin UI
+
+Administrators with the `administer ariada scanner` permission can:
+
+- choose auto, local CLI, or hosted execution mode;
+- set the default scan URL and severity threshold;
+- run a manual scan from the configuration form;
+- review a render-array table of the latest findings.
+
+The module also adds a Drupal Status Report entry showing whether the configured
+scan boundary is available and the latest scan summary.
+
+## Drush
+
+Run a scan from CI or a deployment script:
+
+```bash
+drush ariada:scan https://example.com
+drush ariada:scan https://example.com --severity-threshold=critical --format=json
+```
+
+Exit codes:
+
+- `0`: scan completed and no findings met the threshold;
+- `1`: scan completed and at least one finding met the threshold;
+- `2`: invalid Drush option;
+- `3`: scan failed at the CLI or hosted boundary.
+
+## Update
+
+- Author: Alexander Brichkin (Agonist Development AB)
+- Date: 2026-06-22
diff --git a/integrations/drupal-ariada/ariada_drupal.info.yml b/integrations/drupal-ariada/ariada_drupal.info.yml
new file mode 100644
index 00000000..15df272d
--- /dev/null
+++ b/integrations/drupal-ariada/ariada_drupal.info.yml
@@ -0,0 +1,6 @@
+name: Ariada Accessibility Scanner
+type: module
+description: Runs Ariada accessibility scans from Drupal administration and Drush.
+package: Accessibility
+core_version_requirement: ^10 || ^11
+configure: ariada_drupal.settings
diff --git a/integrations/drupal-ariada/ariada_drupal.install b/integrations/drupal-ariada/ariada_drupal.install
new file mode 100644
index 00000000..19dcc314
--- /dev/null
+++ b/integrations/drupal-ariada/ariada_drupal.install
@@ -0,0 +1,38 @@
+status();
+ $last_summary = \Drupal::state()->get('ariada_drupal.last_scan_summary');
+
+ $requirements['ariada_drupal_scanner'] = [
+ 'title' => t('Ariada scanner'),
+ 'value' => $status['available'] ? t('Ready') : t('Configuration required'),
+ 'description' => t('@message Last scan: @last', [
+ '@message' => $status['message'],
+ '@last' => is_string($last_summary) && $last_summary !== '' ? $last_summary : t('none yet'),
+ ]),
+ 'severity' => $status['available'] ? RequirementSeverity::OK : RequirementSeverity::Warning,
+ ];
+
+ return $requirements;
+}
diff --git a/integrations/drupal-ariada/ariada_drupal.links.menu.yml b/integrations/drupal-ariada/ariada_drupal.links.menu.yml
new file mode 100644
index 00000000..0cd36c17
--- /dev/null
+++ b/integrations/drupal-ariada/ariada_drupal.links.menu.yml
@@ -0,0 +1,6 @@
+ariada_drupal.settings:
+ title: 'Ariada accessibility scanner'
+ description: 'Configure and run Ariada accessibility scans.'
+ parent: system.admin_config_development
+ route_name: ariada_drupal.settings
+ weight: 80
diff --git a/integrations/drupal-ariada/ariada_drupal.permissions.yml b/integrations/drupal-ariada/ariada_drupal.permissions.yml
new file mode 100644
index 00000000..e194b683
--- /dev/null
+++ b/integrations/drupal-ariada/ariada_drupal.permissions.yml
@@ -0,0 +1,4 @@
+administer ariada scanner:
+ title: 'Administer Ariada scanner'
+ description: 'Configure Ariada scan settings and run accessibility scans.'
+ restrict access: true
diff --git a/integrations/drupal-ariada/ariada_drupal.routing.yml b/integrations/drupal-ariada/ariada_drupal.routing.yml
new file mode 100644
index 00000000..121fef96
--- /dev/null
+++ b/integrations/drupal-ariada/ariada_drupal.routing.yml
@@ -0,0 +1,9 @@
+ariada_drupal.settings:
+ path: '/admin/config/development/ariada'
+ defaults:
+ _form: '\Drupal\ariada_drupal\Form\AriadaSettingsForm'
+ _title: 'Ariada accessibility scanner'
+ requirements:
+ _permission: 'administer ariada scanner'
+ options:
+ _admin_route: TRUE
diff --git a/integrations/drupal-ariada/ariada_drupal.services.yml b/integrations/drupal-ariada/ariada_drupal.services.yml
new file mode 100644
index 00000000..f5015136
--- /dev/null
+++ b/integrations/drupal-ariada/ariada_drupal.services.yml
@@ -0,0 +1,21 @@
+services:
+ ariada_drupal.report_normalizer:
+ class: Drupal\ariada_drupal\Service\AriadaReportNormalizer
+ ariada_drupal.local_runner:
+ class: Drupal\ariada_drupal\Service\LocalAriadaRunner
+ arguments:
+ - '@file_system'
+ - '@logger.factory'
+ - '@ariada_drupal.report_normalizer'
+ ariada_drupal.hosted_runner:
+ class: Drupal\ariada_drupal\Service\HostedAriadaRunner
+ arguments:
+ - '@http_client'
+ - '@logger.factory'
+ - '@ariada_drupal.report_normalizer'
+ ariada_drupal.scanner:
+ class: Drupal\ariada_drupal\Service\AriadaScanner
+ arguments:
+ - '@config.factory'
+ - '@ariada_drupal.local_runner'
+ - '@ariada_drupal.hosted_runner'
diff --git a/integrations/drupal-ariada/composer.json b/integrations/drupal-ariada/composer.json
new file mode 100644
index 00000000..95cb4c88
--- /dev/null
+++ b/integrations/drupal-ariada/composer.json
@@ -0,0 +1,18 @@
+{
+ "name": "ariada/drupal-ariada",
+ "description": "Drupal 10/11 module for running Ariada accessibility scans from administration and Drush.",
+ "type": "drupal-module",
+ "license": "GPL-2.0-or-later",
+ "require": {
+ "drupal/core": "^10 || ^11",
+ "php": ">=8.1"
+ },
+ "conflict": {
+ "drush/drush": "<12"
+ },
+ "autoload": {
+ "psr-4": {
+ "Drupal\\ariada_drupal\\": "src/"
+ }
+ }
+}
diff --git a/integrations/drupal-ariada/config/install/ariada_drupal.settings.yml b/integrations/drupal-ariada/config/install/ariada_drupal.settings.yml
new file mode 100644
index 00000000..74c54ee6
--- /dev/null
+++ b/integrations/drupal-ariada/config/install/ariada_drupal.settings.yml
@@ -0,0 +1,7 @@
+execution_mode: auto
+scan_url: ''
+ariada_binary: ariada
+hosted_endpoint: ''
+api_key: ''
+severity_threshold: serious
+timeout_ms: 30000
diff --git a/integrations/drupal-ariada/config/schema/ariada_drupal.schema.yml b/integrations/drupal-ariada/config/schema/ariada_drupal.schema.yml
new file mode 100644
index 00000000..ad1a5776
--- /dev/null
+++ b/integrations/drupal-ariada/config/schema/ariada_drupal.schema.yml
@@ -0,0 +1,25 @@
+ariada_drupal.settings:
+ type: config_object
+ label: 'Ariada scanner settings'
+ mapping:
+ execution_mode:
+ type: string
+ label: 'Execution mode'
+ scan_url:
+ type: string
+ label: 'Default scan URL'
+ ariada_binary:
+ type: string
+ label: 'Ariada CLI binary'
+ hosted_endpoint:
+ type: string
+ label: 'Hosted scan endpoint'
+ api_key:
+ type: string
+ label: 'Hosted scan API key'
+ severity_threshold:
+ type: string
+ label: 'Severity threshold'
+ timeout_ms:
+ type: integer
+ label: 'Scan timeout in milliseconds'
diff --git a/integrations/drupal-ariada/src/Drush/Commands/AriadaDrushCommands.php b/integrations/drupal-ariada/src/Drush/Commands/AriadaDrushCommands.php
new file mode 100644
index 00000000..af71708d
--- /dev/null
+++ b/integrations/drupal-ariada/src/Drush/Commands/AriadaDrushCommands.php
@@ -0,0 +1,140 @@
+ 0,
+ 'moderate' => 1,
+ 'serious' => 2,
+ 'critical' => 3,
+ ];
+
+ public function __construct(
+ #[Autowire('ariada_drupal.scanner')]
+ private readonly AriadaScanner $scanner,
+ #[Autowire('state')]
+ private readonly StateInterface $state,
+ ) {
+ parent::__construct();
+ }
+
+ /**
+ * Runs an Ariada accessibility scan for a URL.
+ */
+ #[CLI\Command(name: 'ariada:scan', aliases: ['ariada-scan'])]
+ #[CLI\Bootstrap(level: DrupalBootLevels::FULL)]
+ #[CLI\Argument(name: 'url', description: 'The http(s) URL to scan.')]
+ #[CLI\Option(name: 'severity-threshold', description: 'Minimum severity that returns exit code 1.', suggestedValues: ['minor', 'moderate', 'serious', 'critical'])]
+ #[CLI\Option(name: 'format', description: 'Output format.', suggestedValues: ['summary', 'json'])]
+ #[CLI\Usage(name: 'drush ariada:scan https://example.com', description: 'Scan a URL with the configured Ariada boundary.')]
+ #[CLI\Usage(name: 'drush ariada:scan https://example.com --severity-threshold=critical --format=json', description: 'Emit JSON and fail only on critical findings.')]
+ public function scan(string $url, array $options = [
+ 'severity-threshold' => 'serious',
+ 'format' => 'summary',
+ ]): int {
+ $threshold = (string) ($options['severity-threshold'] ?? 'serious');
+ $format = (string) ($options['format'] ?? 'summary');
+
+ if (!isset(self::SEVERITY_ORDER[$threshold])) {
+ $this->logger()->error('Invalid severity threshold: {threshold}', [
+ 'threshold' => $threshold,
+ ]);
+ return 2;
+ }
+
+ if (!in_array($format, ['summary', 'json'], TRUE)) {
+ $this->logger()->error('Invalid output format: {format}', [
+ 'format' => $format,
+ ]);
+ return 2;
+ }
+
+ $config = $this->scanner->settings();
+ $config['severity_threshold'] = $threshold;
+
+ $result = $this->scanner->scan($url, $config);
+ $summary = $this->scanner->formatSummary($result);
+ $this->state->set('ariada_drupal.last_scan_result', $result);
+ $this->state->set('ariada_drupal.last_scan_summary', $summary);
+
+ if ($format === 'json') {
+ $this->output()->writeln(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) ?: '{}');
+ }
+ else {
+ $this->io()->writeln(sprintf('Ariada scan: %s', $url));
+ $this->io()->writeln($summary);
+ $this->renderFindingTable((array) ($result['findings'] ?? []));
+ }
+
+ if (empty($result['ok'])) {
+ return 3;
+ }
+
+ return $this->countAtOrAboveThreshold((array) ($result['findings'] ?? []), $threshold) > 0 ? 1 : 0;
+ }
+
+ /**
+ * Prints a compact finding table for summary output.
+ */
+ private function renderFindingTable(array $findings): void {
+ $rows = [];
+ foreach (array_slice($findings, 0, 25) as $finding) {
+ $finding = (array) $finding;
+ $rows[] = [
+ (string) ($finding['severity'] ?? ''),
+ (string) ($finding['rule'] ?? ''),
+ (string) ($finding['message'] ?? ''),
+ (string) ($finding['target'] ?? ''),
+ ];
+ }
+
+ if ($rows === []) {
+ $this->io()->writeln('No findings returned.');
+ return;
+ }
+
+ $this->io()->table(['Severity', 'Rule', 'Finding', 'Target'], $rows);
+ }
+
+ /**
+ * Counts findings whose severity breaches the selected threshold.
+ */
+ private function countAtOrAboveThreshold(array $findings, string $threshold): int {
+ $minimum = self::SEVERITY_ORDER[$threshold] ?? self::SEVERITY_ORDER['serious'];
+ $count = 0;
+
+ foreach ($findings as $finding) {
+ $finding = (array) $finding;
+ $severity = (string) ($finding['severity'] ?? 'moderate');
+ if ((self::SEVERITY_ORDER[$severity] ?? self::SEVERITY_ORDER['moderate']) >= $minimum) {
+ $count++;
+ }
+ }
+
+ return $count;
+ }
+
+}
diff --git a/integrations/drupal-ariada/src/Form/AriadaSettingsForm.php b/integrations/drupal-ariada/src/Form/AriadaSettingsForm.php
new file mode 100644
index 00000000..58ffabaa
--- /dev/null
+++ b/integrations/drupal-ariada/src/Form/AriadaSettingsForm.php
@@ -0,0 +1,270 @@
+get('config.factory'),
+ $container->get('ariada_drupal.scanner'),
+ $container->get('state'),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId(): string {
+ return 'ariada_drupal_settings_form';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getEditableConfigNames(): array {
+ return ['ariada_drupal.settings'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state): array {
+ $config = $this->config('ariada_drupal.settings');
+
+ $form['scan_url'] = [
+ '#type' => 'url',
+ '#title' => $this->t('Default scan URL'),
+ '#default_value' => $config->get('scan_url') ?: '',
+ '#description' => $this->t('Used by the manual admin scan when no other URL is supplied.'),
+ '#maxlength' => 2048,
+ ];
+
+ $form['execution_mode'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Execution mode'),
+ '#default_value' => $config->get('execution_mode') ?: 'auto',
+ '#options' => [
+ 'auto' => $this->t('Auto: local CLI, then hosted endpoint'),
+ 'local' => $this->t('Local Ariada CLI'),
+ 'hosted' => $this->t('Hosted scan endpoint'),
+ ],
+ '#required' => TRUE,
+ ];
+
+ $form['ariada_binary'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Ariada CLI binary'),
+ '#default_value' => $config->get('ariada_binary') ?: 'ariada',
+ '#description' => $this->t('Binary name or absolute path. Install with npm install -g @ariada-org/cli, or point to a built local binary.'),
+ '#maxlength' => 512,
+ ];
+
+ $form['severity_threshold'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Severity threshold'),
+ '#default_value' => $config->get('severity_threshold') ?: 'serious',
+ '#options' => [
+ 'minor' => $this->t('Minor'),
+ 'moderate' => $this->t('Moderate'),
+ 'serious' => $this->t('Serious'),
+ 'critical' => $this->t('Critical'),
+ ],
+ '#required' => TRUE,
+ ];
+
+ $form['timeout_ms'] = [
+ '#type' => 'number',
+ '#title' => $this->t('Local scan timeout'),
+ '#default_value' => (int) ($config->get('timeout_ms') ?: 30000),
+ '#min' => 1000,
+ '#step' => 1000,
+ '#field_suffix' => $this->t('milliseconds'),
+ ];
+
+ $form['hosted'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Hosted scan endpoint'),
+ '#open' => (bool) $config->get('hosted_endpoint'),
+ ];
+
+ $form['hosted']['hosted_endpoint'] = [
+ '#type' => 'url',
+ '#title' => $this->t('Endpoint base URL'),
+ '#default_value' => $config->get('hosted_endpoint') ?: '',
+ '#description' => $this->t('Optional hosted scanner base URL. The module calls /api/scan below this URL.'),
+ '#maxlength' => 2048,
+ ];
+
+ $form['hosted']['api_key'] = [
+ '#type' => 'password',
+ '#title' => $this->t('API key'),
+ '#description' => $this->t('Leave blank to keep the existing key.'),
+ '#maxlength' => 512,
+ ];
+
+ $result = $form_state->get('ariada_result');
+ if (!is_array($result)) {
+ $result = $this->state->get('ariada_drupal.last_scan_result');
+ }
+
+ if (is_array($result)) {
+ $form['latest_result'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Latest scan result'),
+ '#open' => TRUE,
+ ];
+ $form['latest_result']['summary'] = [
+ '#plain_text' => $this->scanner->formatSummary($result),
+ ];
+ $form['latest_result']['findings'] = $this->buildFindingsTable($result);
+ }
+
+ $form = parent::buildForm($form, $form_state);
+ $form['actions']['run_scan'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Save and scan'),
+ '#submit' => ['::scanSubmit'],
+ '#button_type' => 'primary',
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state): void {
+ parent::validateForm($form, $form_state);
+
+ $timeout = (int) $form_state->getValue('timeout_ms');
+ if ($timeout < 1000) {
+ $form_state->setErrorByName('timeout_ms', $this->t('Timeout must be at least 1000 milliseconds.'));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state): void {
+ $this->saveSettings($form_state);
+ parent::submitForm($form, $form_state);
+ }
+
+ /**
+ * Saves settings and immediately runs a scan.
+ */
+ public function scanSubmit(array &$form, FormStateInterface $form_state): void {
+ $this->saveSettings($form_state);
+
+ $url = (string) $form_state->getValue('scan_url');
+ if ($url === '') {
+ $url = \Drupal::request()->getSchemeAndHttpHost();
+ }
+
+ $result = $this->scanner->scan($url);
+ $summary = $this->scanner->formatSummary($result);
+ $this->state->set('ariada_drupal.last_scan_result', $result);
+ $this->state->set('ariada_drupal.last_scan_summary', $summary);
+
+ if (!empty($result['ok'])) {
+ $this->messenger()->addStatus($this->t('Ariada scan completed: @summary', [
+ '@summary' => $summary,
+ ]));
+ }
+ else {
+ $this->messenger()->addError($this->t('Ariada scan failed: @message', [
+ '@message' => (string) ($result['error'] ?? 'unknown error'),
+ ]));
+ }
+
+ $form_state->set('ariada_result', $result);
+ $form_state->setRebuild(TRUE);
+ }
+
+ /**
+ * Persists form values.
+ */
+ private function saveSettings(FormStateInterface $form_state): void {
+ $config = $this->config('ariada_drupal.settings');
+ $apiKey = (string) $form_state->getValue('api_key');
+
+ $config
+ ->set('execution_mode', (string) $form_state->getValue('execution_mode'))
+ ->set('scan_url', (string) $form_state->getValue('scan_url'))
+ ->set('ariada_binary', (string) $form_state->getValue('ariada_binary'))
+ ->set('hosted_endpoint', (string) $form_state->getValue('hosted_endpoint'))
+ ->set('severity_threshold', (string) $form_state->getValue('severity_threshold'))
+ ->set('timeout_ms', (int) $form_state->getValue('timeout_ms'));
+
+ if ($apiKey !== '') {
+ $config->set('api_key', $apiKey);
+ }
+
+ $config->save();
+ }
+
+ /**
+ * Builds a render array table for normalized findings.
+ *
+ * @return array
+ */
+ private function buildFindingsTable(array $result): array {
+ $findings = array_slice((array) ($result['findings'] ?? []), 0, 50);
+ if ($findings === []) {
+ return [
+ '#plain_text' => $this->t('No findings were returned.'),
+ ];
+ }
+
+ $rows = [];
+ foreach ($findings as $finding) {
+ $finding = (array) $finding;
+ $rows[] = [
+ (string) ($finding['severity'] ?? ''),
+ (string) ($finding['rule'] ?? ''),
+ (string) ($finding['message'] ?? ''),
+ (string) ($finding['target'] ?? ''),
+ ];
+ }
+
+ return [
+ '#type' => 'table',
+ '#header' => [
+ $this->t('Severity'),
+ $this->t('Rule'),
+ $this->t('Finding'),
+ $this->t('Target'),
+ ],
+ '#rows' => $rows,
+ '#empty' => $this->t('No findings were returned.'),
+ ];
+ }
+
+}
diff --git a/integrations/drupal-ariada/src/Service/AriadaReportNormalizer.php b/integrations/drupal-ariada/src/Service/AriadaReportNormalizer.php
new file mode 100644
index 00000000..b7180164
--- /dev/null
+++ b/integrations/drupal-ariada/src/Service/AriadaReportNormalizer.php
@@ -0,0 +1,159 @@
+
+ */
+ public function normalizeJson(string $json, string $source, int $exitCode): array {
+ return $this->normalize(Json::decode($json), $source, $exitCode);
+ }
+
+ /**
+ * Normalizes the CLI/API response into a form-friendly result.
+ *
+ * @return array
+ */
+ public function normalize(mixed $report, string $source, int $exitCode): array {
+ if (!is_array($report)) {
+ return [
+ 'ok' => FALSE,
+ 'source' => $source,
+ 'exit_code' => $exitCode,
+ 'error' => 'Scan returned invalid JSON.',
+ ];
+ }
+
+ $findings = $this->flattenFindings($report);
+ $summary = (array) ($report['summary'] ?? []);
+ $summary['total'] = (int) ($summary['total'] ?? count($findings));
+ $summary['byImpact'] = (array) ($summary['byImpact'] ?? $this->countByImpact($findings));
+
+ return [
+ 'ok' => TRUE,
+ 'source' => $source,
+ 'exit_code' => $exitCode,
+ 'summary' => $summary,
+ 'findings' => $findings,
+ 'report' => $report,
+ ];
+ }
+
+ /**
+ * Extracts displayable findings from known Ariada report shapes.
+ *
+ * @return array>
+ */
+ private function flattenFindings(array $report): array {
+ $raw = $report['findings'] ?? ($report['report']['findings'] ?? NULL);
+ if (is_array($raw)) {
+ return $this->normalizeFindingList($this->flattenList($raw));
+ }
+
+ $grid = $report['grid'] ?? ($report['report']['grid'] ?? NULL);
+ return is_array($grid) ? $this->normalizeFindingList($this->flattenList($grid)) : [];
+ }
+
+ /**
+ * Flattens nested associative finding buckets.
+ *
+ * @return array
+ */
+ private function flattenList(array $items): array {
+ $out = [];
+ foreach ($items as $item) {
+ if (is_array($item) && $this->looksLikeFinding($item)) {
+ $out[] = $item;
+ }
+ elseif (is_array($item)) {
+ $out = array_merge($out, $this->flattenList($item));
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * Returns normalized finding rows for render arrays and Drush output.
+ *
+ * @return array>
+ */
+ private function normalizeFindingList(array $findings): array {
+ $rows = [];
+ foreach ($findings as $finding) {
+ if (!is_array($finding)) {
+ continue;
+ }
+ $target = $finding['selector'] ?? $finding['target'] ?? $finding['element'] ?? '';
+ $rows[] = [
+ 'severity' => (string) ($finding['severity'] ?? $finding['impact'] ?? 'moderate'),
+ 'rule' => (string) ($finding['ruleId'] ?? $finding['rule_id'] ?? $finding['id'] ?? 'unknown'),
+ 'message' => (string) ($finding['message'] ?? $finding['description'] ?? $finding['help'] ?? ''),
+ 'target' => is_array($target) ? implode(', ', array_map('strval', $target)) : (string) $target,
+ ];
+ }
+ return $rows;
+ }
+
+ /**
+ * Counts findings by normalized impact.
+ *
+ * @return array
+ */
+ private function countByImpact(array $findings): array {
+ $counts = ['critical' => 0, 'serious' => 0, 'moderate' => 0, 'minor' => 0];
+ foreach ($findings as $finding) {
+ $severity = (string) ($finding['severity'] ?? 'moderate');
+ if (isset($counts[$severity])) {
+ $counts[$severity]++;
+ }
+ }
+ return $counts;
+ }
+
+ /**
+ * Detects whether an array resembles a finding.
+ */
+ private function looksLikeFinding(array $item): bool {
+ return isset($item['ruleId'])
+ || isset($item['rule_id'])
+ || isset($item['severity'])
+ || isset($item['impact'])
+ || isset($item['message']);
+ }
+
+}
diff --git a/integrations/drupal-ariada/src/Service/AriadaScanner.php b/integrations/drupal-ariada/src/Service/AriadaScanner.php
new file mode 100644
index 00000000..8af8bcf4
--- /dev/null
+++ b/integrations/drupal-ariada/src/Service/AriadaScanner.php
@@ -0,0 +1,123 @@
+
+ */
+ public function scan(string $url, ?array $overrideConfig = NULL): array {
+ $url = trim($url);
+ if (!$this->isValidHttpUrl($url)) {
+ return [
+ 'ok' => FALSE,
+ 'source' => 'validation',
+ 'error' => 'Enter a valid http(s) URL.',
+ ];
+ }
+
+ $config = $overrideConfig ?? $this->settings();
+ $mode = (string) ($config['execution_mode'] ?? 'auto');
+
+ if ($mode === 'hosted') {
+ return $this->hostedRunner->scan($url, $config);
+ }
+
+ if ($mode === 'local') {
+ return $this->localRunner->scan($url, $config);
+ }
+
+ if ($this->localRunner->canRun((string) ($config['ariada_binary'] ?? 'ariada'))) {
+ return $this->localRunner->scan($url, $config);
+ }
+
+ return $this->hostedRunner->scan($url, $config);
+ }
+
+ /**
+ * Returns a status summary for Drupal's status report.
+ *
+ * @return array{available: bool, message: string}
+ */
+ public function status(): array {
+ $config = $this->settings();
+ $mode = (string) ($config['execution_mode'] ?? 'auto');
+ $binary = (string) ($config['ariada_binary'] ?? 'ariada');
+ $local = $this->localRunner->canRun($binary);
+ $hosted = $this->hostedRunner->hasConfig($config);
+
+ if ($mode === 'hosted') {
+ return [
+ 'available' => $hosted,
+ 'message' => $hosted
+ ? 'Hosted scan endpoint is configured.'
+ : 'Hosted mode requires an endpoint and API key.',
+ ];
+ }
+
+ if ($mode === 'local') {
+ return [
+ 'available' => $local,
+ 'message' => $local
+ ? 'Local Ariada CLI is callable.'
+ : 'Local mode requires proc_open and the Ariada CLI binary.',
+ ];
+ }
+
+ return [
+ 'available' => $local || $hosted,
+ 'message' => $local
+ ? 'Auto mode will use the local Ariada CLI.'
+ : ($hosted
+ ? 'Auto mode will use the hosted scan endpoint.'
+ : 'Auto mode needs either a local Ariada CLI or hosted endpoint settings.'),
+ ];
+ }
+
+ /**
+ * Returns module settings as a plain array.
+ *
+ * @return array
+ */
+ public function settings(): array {
+ return $this->configFactory->get('ariada_drupal.settings')->getRawData();
+ }
+
+ /**
+ * Formats a short one-line scan summary.
+ */
+ public function formatSummary(array $result): string {
+ return AriadaReportNormalizer::formatSummary($result);
+ }
+
+ /**
+ * Validates URL input.
+ */
+ private function isValidHttpUrl(string $url): bool {
+ $parts = parse_url($url);
+ return is_array($parts)
+ && isset($parts['scheme'], $parts['host'])
+ && in_array($parts['scheme'], ['http', 'https'], TRUE);
+ }
+
+}
diff --git a/integrations/drupal-ariada/src/Service/HostedAriadaRunner.php b/integrations/drupal-ariada/src/Service/HostedAriadaRunner.php
new file mode 100644
index 00000000..21d85629
--- /dev/null
+++ b/integrations/drupal-ariada/src/Service/HostedAriadaRunner.php
@@ -0,0 +1,124 @@
+
+ */
+ public function scan(string $url, array $config): array {
+ if (!$this->hasConfig($config)) {
+ return $this->error('Hosted scanning requires an endpoint and API key.');
+ }
+
+ $endpoint = rtrim((string) $config['hosted_endpoint'], '/');
+ $apiKey = (string) $config['api_key'];
+
+ try {
+ $response = $this->httpClient->post($endpoint . '/api/scan', [
+ 'headers' => [
+ 'Authorization' => 'Bearer ' . $apiKey,
+ 'Accept' => 'application/json',
+ ],
+ 'json' => [
+ 'url' => $url,
+ 'severityThreshold' => (string) ($config['severity_threshold'] ?? 'serious'),
+ ],
+ 'timeout' => 30,
+ ]);
+
+ $body = Json::decode((string) $response->getBody());
+ if (is_array($body) && isset($body['id']) && !isset($body['report'])) {
+ return $this->poll($endpoint, $apiKey, (string) $body['id']);
+ }
+
+ return $this->normalizer->normalize($body, 'hosted', 0);
+ }
+ catch (GuzzleException $exception) {
+ $this->loggerFactory->get(self::LOGGER_CHANNEL)->warning('Hosted Ariada scan failed: @message', [
+ '@message' => $exception->getMessage(),
+ ]);
+ return $this->error($exception->getMessage());
+ }
+ }
+
+ /**
+ * Checks whether hosted settings are complete.
+ */
+ public function hasConfig(array $config): bool {
+ return !empty($config['hosted_endpoint']) && !empty($config['api_key']);
+ }
+
+ /**
+ * Polls an asynchronous hosted scan.
+ *
+ * @return array
+ */
+ private function poll(string $endpoint, string $apiKey, string $scanId): array {
+ try {
+ for ($attempt = 0; $attempt < 12; $attempt++) {
+ sleep(2);
+ $response = $this->httpClient->get($endpoint . '/api/scan/' . rawurlencode($scanId), [
+ 'headers' => [
+ 'Authorization' => 'Bearer ' . $apiKey,
+ 'Accept' => 'application/json',
+ ],
+ 'timeout' => 15,
+ ]);
+ $body = Json::decode((string) $response->getBody());
+
+ if (($body['status'] ?? '') === 'done') {
+ return $this->normalizer->normalize((array) ($body['report'] ?? $body), 'hosted', 0);
+ }
+
+ if (($body['status'] ?? '') === 'error') {
+ return $this->error((string) ($body['message'] ?? 'Hosted scan returned an error.'));
+ }
+ }
+ }
+ catch (GuzzleException $exception) {
+ return $this->error($exception->getMessage());
+ }
+
+ return $this->error('Hosted scan did not finish before the polling timeout.');
+ }
+
+ /**
+ * Builds a hosted-runner error result.
+ *
+ * @return array
+ */
+ private function error(string $message): array {
+ return [
+ 'ok' => FALSE,
+ 'source' => 'hosted',
+ 'error' => $message,
+ ];
+ }
+
+}
diff --git a/integrations/drupal-ariada/src/Service/LocalAriadaRunner.php b/integrations/drupal-ariada/src/Service/LocalAriadaRunner.php
new file mode 100644
index 00000000..c1be67a1
--- /dev/null
+++ b/integrations/drupal-ariada/src/Service/LocalAriadaRunner.php
@@ -0,0 +1,179 @@
+
+ */
+ public function scan(string $url, array $config): array {
+ if (!function_exists('proc_open')) {
+ return $this->error('proc_open is not available.');
+ }
+
+ $outputDir = $this->createOutputDirectory();
+ if ($outputDir === NULL) {
+ return $this->error('Could not create a temporary Ariada output directory.');
+ }
+
+ $process = @proc_open(
+ $this->buildCommand($url, $config, $outputDir),
+ [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
+ $pipes,
+ );
+
+ if (!is_resource($process)) {
+ $this->deleteDirectory($outputDir);
+ return $this->error('Could not start the Ariada CLI process.');
+ }
+
+ fclose($pipes[0]);
+ $stdout = stream_get_contents($pipes[1]) ?: '';
+ $stderr = stream_get_contents($pipes[2]) ?: '';
+ fclose($pipes[1]);
+ fclose($pipes[2]);
+ $exitCode = proc_close($process);
+
+ $scanFile = $outputDir . DIRECTORY_SEPARATOR . 'scan.json';
+ $json = is_file($scanFile) ? file_get_contents($scanFile) : FALSE;
+ $this->deleteDirectory($outputDir);
+
+ if (in_array($exitCode, [0, 1], TRUE) && is_string($json) && $json !== '') {
+ return $this->normalizer->normalizeJson($json, 'local', $exitCode);
+ }
+
+ $message = trim($stderr) !== '' ? trim($stderr) : trim($stdout);
+ $this->loggerFactory->get(self::LOGGER_CHANNEL)->warning('Ariada CLI failed with code @code: @message', [
+ '@code' => (string) $exitCode,
+ '@message' => $message,
+ ]);
+ return $this->error($message !== '' ? $message : sprintf('Ariada CLI exited with code %d.', $exitCode), $exitCode);
+ }
+
+ /**
+ * Checks the local CLI boundary.
+ */
+ public function canRun(string $binary): bool {
+ if (!function_exists('proc_open')) {
+ return FALSE;
+ }
+
+ $process = @proc_open(
+ [$binary, 'version'],
+ [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
+ $pipes,
+ );
+
+ if (!is_resource($process)) {
+ return FALSE;
+ }
+
+ fclose($pipes[0]);
+ stream_get_contents($pipes[1]);
+ stream_get_contents($pipes[2]);
+ fclose($pipes[1]);
+ fclose($pipes[2]);
+ return proc_close($process) === 0;
+ }
+
+ /**
+ * Builds the Ariada CLI command.
+ *
+ * @return array
+ */
+ private function buildCommand(string $url, array $config, string $outputDir): array {
+ return [
+ (string) ($config['ariada_binary'] ?? 'ariada'),
+ 'scan',
+ $url,
+ '--format',
+ 'json',
+ '--output-dir',
+ $outputDir,
+ '--severity-threshold',
+ $this->normalizeThreshold((string) ($config['severity_threshold'] ?? 'serious')),
+ '--timeout-ms',
+ (string) max(1000, (int) ($config['timeout_ms'] ?? 30000)),
+ ];
+ }
+
+ /**
+ * Creates a temporary directory for CLI output.
+ */
+ private function createOutputDirectory(): ?string {
+ $base = $this->fileSystem->getTempDirectory() ?: sys_get_temp_dir();
+ $dir = $base . DIRECTORY_SEPARATOR . 'ariada-drupal-' . bin2hex(random_bytes(6));
+
+ if (!mkdir($dir, 0700, TRUE) && !is_dir($dir)) {
+ return NULL;
+ }
+
+ return $dir;
+ }
+
+ /**
+ * Removes a temporary output directory.
+ */
+ private function deleteDirectory(string $dir): void {
+ if (!is_dir($dir)) {
+ return;
+ }
+
+ $files = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
+ \RecursiveIteratorIterator::CHILD_FIRST,
+ );
+ foreach ($files as $file) {
+ $path = $file->getRealPath();
+ if ($path !== FALSE) {
+ $file->isDir() ? rmdir($path) : unlink($path);
+ }
+ }
+ rmdir($dir);
+ }
+
+ /**
+ * Normalizes an unsupported threshold to the default CI-safe level.
+ */
+ private function normalizeThreshold(string $threshold): string {
+ return in_array($threshold, ['minor', 'moderate', 'serious', 'critical'], TRUE) ? $threshold : 'serious';
+ }
+
+ /**
+ * Builds a local-runner error result.
+ *
+ * @return array
+ */
+ private function error(string $message, int $exitCode = 3): array {
+ return [
+ 'ok' => FALSE,
+ 'source' => 'local',
+ 'exit_code' => $exitCode,
+ 'error' => $message,
+ ];
+ }
+
+}
diff --git a/integrations/eclipse-ariada/.gitignore b/integrations/eclipse-ariada/.gitignore
new file mode 100644
index 00000000..567609b1
--- /dev/null
+++ b/integrations/eclipse-ariada/.gitignore
@@ -0,0 +1 @@
+build/
diff --git a/integrations/eclipse-ariada/META-INF/MANIFEST.MF b/integrations/eclipse-ariada/META-INF/MANIFEST.MF
new file mode 100644
index 00000000..e6f5c224
--- /dev/null
+++ b/integrations/eclipse-ariada/META-INF/MANIFEST.MF
@@ -0,0 +1,7 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: Ariada Eclipse
+Bundle-SymbolicName: org.ariada.eclipse
+Bundle-Version: 0.1.0.qualifier
+Bundle-Vendor: Ariada
+Bundle-RequiredExecutionEnvironment: JavaSE-17
diff --git a/integrations/eclipse-ariada/README.md b/integrations/eclipse-ariada/README.md
new file mode 100644
index 00000000..771e69c5
--- /dev/null
+++ b/integrations/eclipse-ariada/README.md
@@ -0,0 +1,29 @@
+# Ariada Eclipse Plugin
+
+Eclipse plugin scaffold for turning Ariada CLI findings into IDE markers. It is
+thin by design: Ariada CLI owns scanning, and the plugin maps returned findings
+onto Eclipse marker concepts.
+
+## What It Does
+
+- Defines plugin metadata and a command id.
+- Builds Java model classes for Ariada findings.
+- Maps Ariada impact levels to Eclipse marker severities.
+
+## Local Gates
+
+```sh
+bash scripts/compile-smoke.sh
+```
+
+Tycho/Maven packaging is blocked because Maven is not installed on this machine.
+The local smoke compiles the Java mapper and runs a fixture assertion with
+`javac/java`.
+
+## Live-Host Blocker
+
+Blocked: Eclipse Marketplace publication requires Tycho build output, an update
+site, Marketplace account access, and store review.
+
+Owner: founder. Next action: run Maven/Tycho packaging on a host with Maven and
+Eclipse PDE dependencies, then submit the update site to Marketplace.
diff --git a/integrations/eclipse-ariada/plugin.xml b/integrations/eclipse-ariada/plugin.xml
new file mode 100644
index 00000000..ad9a3971
--- /dev/null
+++ b/integrations/eclipse-ariada/plugin.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/integrations/eclipse-ariada/scripts/compile-smoke.sh b/integrations/eclipse-ariada/scripts/compile-smoke.sh
new file mode 100755
index 00000000..390e7899
--- /dev/null
+++ b/integrations/eclipse-ariada/scripts/compile-smoke.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+OUT="$ROOT/build/classes"
+rm -rf "$ROOT/build"
+mkdir -p "$OUT"
+
+javac -d "$OUT" \
+ "$ROOT/src/org/ariada/eclipse/AriadaFinding.java" \
+ "$ROOT/src/org/ariada/eclipse/AriadaMarker.java" \
+ "$ROOT/src/org/ariada/eclipse/AriadaMarkerMapper.java" \
+ "$ROOT/test/org/ariada/eclipse/AriadaMarkerMapperSmoke.java"
+
+java -cp "$OUT" org.ariada.eclipse.AriadaMarkerMapperSmoke
diff --git a/integrations/eclipse-ariada/src/org/ariada/eclipse/AriadaFinding.java b/integrations/eclipse-ariada/src/org/ariada/eclipse/AriadaFinding.java
new file mode 100644
index 00000000..430289be
--- /dev/null
+++ b/integrations/eclipse-ariada/src/org/ariada/eclipse/AriadaFinding.java
@@ -0,0 +1,4 @@
+package org.ariada.eclipse;
+
+public record AriadaFinding(String file, int line, int column, String impact, String ruleId, String message) {
+}
diff --git a/integrations/eclipse-ariada/src/org/ariada/eclipse/AriadaMarker.java b/integrations/eclipse-ariada/src/org/ariada/eclipse/AriadaMarker.java
new file mode 100644
index 00000000..d4df2dab
--- /dev/null
+++ b/integrations/eclipse-ariada/src/org/ariada/eclipse/AriadaMarker.java
@@ -0,0 +1,4 @@
+package org.ariada.eclipse;
+
+public record AriadaMarker(String file, int line, int column, int severity, String message) {
+}
diff --git a/integrations/eclipse-ariada/src/org/ariada/eclipse/AriadaMarkerMapper.java b/integrations/eclipse-ariada/src/org/ariada/eclipse/AriadaMarkerMapper.java
new file mode 100644
index 00000000..1319bc68
--- /dev/null
+++ b/integrations/eclipse-ariada/src/org/ariada/eclipse/AriadaMarkerMapper.java
@@ -0,0 +1,27 @@
+package org.ariada.eclipse;
+
+public final class AriadaMarkerMapper {
+ public static final int INFO = 1;
+ public static final int WARNING = 2;
+ public static final int ERROR = 3;
+
+ private AriadaMarkerMapper() {
+ }
+
+ public static AriadaMarker toMarker(AriadaFinding finding) {
+ return new AriadaMarker(
+ finding.file(),
+ finding.line(),
+ finding.column(),
+ severityForImpact(finding.impact()),
+ finding.ruleId() + ": " + finding.message());
+ }
+
+ public static int severityForImpact(String impact) {
+ return switch (impact == null ? "" : impact.toLowerCase()) {
+ case "critical", "serious" -> ERROR;
+ case "moderate" -> WARNING;
+ default -> INFO;
+ };
+ }
+}
diff --git a/integrations/eclipse-ariada/test/org/ariada/eclipse/AriadaMarkerMapperSmoke.java b/integrations/eclipse-ariada/test/org/ariada/eclipse/AriadaMarkerMapperSmoke.java
new file mode 100644
index 00000000..b043690c
--- /dev/null
+++ b/integrations/eclipse-ariada/test/org/ariada/eclipse/AriadaMarkerMapperSmoke.java
@@ -0,0 +1,18 @@
+package org.ariada.eclipse;
+
+public final class AriadaMarkerMapperSmoke {
+ private AriadaMarkerMapperSmoke() {
+ }
+
+ public static void main(String[] args) {
+ AriadaFinding finding = new AriadaFinding("index.html", 12, 4, "serious", "image-alt", "Images need alt text");
+ AriadaMarker marker = AriadaMarkerMapper.toMarker(finding);
+ if (marker.severity() != AriadaMarkerMapper.ERROR) {
+ throw new IllegalStateException("serious findings must map to error markers");
+ }
+ if (!marker.message().contains("image-alt")) {
+ throw new IllegalStateException("marker message must include Ariada rule id");
+ }
+ System.out.println("PASS Eclipse marker mapper smoke");
+ }
+}
diff --git a/integrations/edge-addon-ariada/.gitignore b/integrations/edge-addon-ariada/.gitignore
new file mode 100644
index 00000000..f0721046
--- /dev/null
+++ b/integrations/edge-addon-ariada/.gitignore
@@ -0,0 +1,3 @@
+dist/
+*.zip
+
diff --git a/integrations/edge-addon-ariada/README.md b/integrations/edge-addon-ariada/README.md
new file mode 100644
index 00000000..80e104c8
--- /dev/null
+++ b/integrations/edge-addon-ariada/README.md
@@ -0,0 +1,34 @@
+# Ariada Edge Add-ons Packaging
+
+This integration packages the existing Ariada browser extension for Microsoft
+Edge Add-ons. It does not contain scan logic. The source extension remains
+`packages/extension-chrome`.
+
+## What It Does
+
+- Reads the built MV3 extension from `packages/extension-chrome/.output/chrome-mv3`.
+- Validates that the manifest is Edge-store compatible.
+- Copies the build into `dist/edge-mv3`.
+- Produces `dist/ariada-edge-addon.zip` for Partner Center upload.
+
+## Local Gates
+
+```sh
+node integrations/edge-addon-ariada/scripts/validate-edge-package.mjs
+node integrations/edge-addon-ariada/scripts/build-edge-package.mjs
+```
+
+If the source build is missing, run:
+
+```sh
+pnpm -F @ariada-org/extension-chrome build
+```
+
+## Live-Host Blocker
+
+Blocked: Microsoft Edge Add-ons submission requires Partner Center access and
+store review.
+
+Owner: founder. Next action: sign in to Partner Center, create the Edge Add-ons
+listing, upload `dist/ariada-edge-addon.zip`, and complete store review.
+
diff --git a/integrations/edge-addon-ariada/package.json b/integrations/edge-addon-ariada/package.json
new file mode 100644
index 00000000..31f0a107
--- /dev/null
+++ b/integrations/edge-addon-ariada/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@ariada-integrations/edge-addon-ariada",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "node scripts/build-edge-package.mjs",
+ "test": "node scripts/validate-edge-package.mjs"
+ }
+}
diff --git a/integrations/edge-addon-ariada/scripts/build-edge-package.mjs b/integrations/edge-addon-ariada/scripts/build-edge-package.mjs
new file mode 100644
index 00000000..f9c2c123
--- /dev/null
+++ b/integrations/edge-addon-ariada/scripts/build-edge-package.mjs
@@ -0,0 +1,22 @@
+#!/usr/bin/env node
+import { cp, mkdir, rm } from 'node:fs/promises';
+import { spawnSync } from 'node:child_process';
+import { resolve } from 'node:path';
+
+const root = resolve(import.meta.dirname, '../../..');
+const source = resolve(root, 'packages/extension-chrome/.output/chrome-mv3');
+const dist = resolve(import.meta.dirname, '../dist');
+const packageDir = resolve(dist, 'edge-mv3');
+const zipPath = resolve(dist, 'ariada-edge-addon.zip');
+
+await rm(dist, { recursive: true, force: true });
+await mkdir(dist, { recursive: true });
+await cp(source, packageDir, { recursive: true });
+
+const zip = spawnSync('zip', ['-qr', zipPath, '.'], { cwd: packageDir, stdio: 'inherit' });
+if (zip.status !== 0) {
+ process.exit(zip.status ?? 1);
+}
+
+console.log(`PASS wrote ${zipPath}`);
+
diff --git a/integrations/edge-addon-ariada/scripts/validate-edge-package.mjs b/integrations/edge-addon-ariada/scripts/validate-edge-package.mjs
new file mode 100644
index 00000000..5517100b
--- /dev/null
+++ b/integrations/edge-addon-ariada/scripts/validate-edge-package.mjs
@@ -0,0 +1,24 @@
+#!/usr/bin/env node
+import { readFile } from 'node:fs/promises';
+import { resolve } from 'node:path';
+
+const root = resolve(import.meta.dirname, '../../..');
+const manifestPath = resolve(root, 'packages/extension-chrome/.output/chrome-mv3/manifest.json');
+const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
+
+const failures = [];
+if (manifest.manifest_version !== 3) failures.push('manifest_version must be 3');
+if (!manifest.name || !manifest.version || !manifest.description) failures.push('name, version, and description are required');
+if (!manifest.action?.default_popup) failures.push('action.default_popup must point to the existing popup');
+if (!manifest.background?.service_worker) failures.push('background.service_worker must reuse the existing extension worker');
+if (!manifest.permissions?.includes('activeTab')) failures.push('activeTab permission is required for tab scans');
+if (!manifest.permissions?.includes('scripting')) failures.push('scripting permission is required for injection');
+if (manifest.externally_connectable) failures.push('externally_connectable is not needed for the Edge listing');
+
+if (failures.length > 0) {
+ console.error(`Edge package validation failed:\n- ${failures.join('\n- ')}`);
+ process.exit(1);
+}
+
+console.log(`PASS Edge MV3 manifest validation: ${manifest.name} ${manifest.version}`);
+
diff --git a/integrations/edge-addon-ariada/store-listing.json b/integrations/edge-addon-ariada/store-listing.json
new file mode 100644
index 00000000..36f24c22
--- /dev/null
+++ b/integrations/edge-addon-ariada/store-listing.json
@@ -0,0 +1,8 @@
+{
+ "name": "ariada - accessibility scanner",
+ "shortDescription": "Run local accessibility scans from the browser extension.",
+ "category": "Developer tools",
+ "privacy": "Local scans only; page content is not sent to Ariada by this package.",
+ "sourceBuild": "../../packages/extension-chrome/.output/chrome-mv3",
+ "submissionBlocker": "Microsoft Partner Center account and Edge Add-ons review"
+}
diff --git a/integrations/elixir-phoenix-ariada/.formatter.exs b/integrations/elixir-phoenix-ariada/.formatter.exs
new file mode 100644
index 00000000..ba35ec77
--- /dev/null
+++ b/integrations/elixir-phoenix-ariada/.formatter.exs
@@ -0,0 +1,6 @@
+[
+ inputs: [
+ "{mix,.formatter}.exs",
+ "{config,lib,test}/**/*.{ex,exs}"
+ ]
+]
diff --git a/integrations/elixir-phoenix-ariada/README.md b/integrations/elixir-phoenix-ariada/README.md
new file mode 100644
index 00000000..5511027d
--- /dev/null
+++ b/integrations/elixir-phoenix-ariada/README.md
@@ -0,0 +1,46 @@
+# Ariada Phoenix
+
+`ariada_phoenix` is a thin Hex package for Phoenix applications. It adds
+`mix ariada.scan`, which shells out to the shared `@ariada-org/cli` scanner and
+parses the JSON result for CI-friendly gate output.
+
+It does not implement accessibility scanning in Elixir. The scanner remains the
+shared Ariada CLI:
+
+```sh
+npm install -g @ariada-org/cli
+mix ariada.scan --url http://localhost:4000 --max-violations 0
+```
+
+## Phoenix fit
+
+Phoenix applications commonly expose rendered HTML at `http://localhost:4000`
+in development. The mix task defaults to that URL unless `--url`, `--path`, or
+the `:ariada_phoenix, :base_url` application config overrides it.
+
+## Options
+
+```sh
+mix ariada.scan --url http://localhost:4000
+mix ariada.scan --path priv/static/index.html
+mix ariada.scan --cli /path/to/ariada --max-violations 3
+```
+
+- `--url` scans a running Phoenix/Phoenix LiveView surface.
+- `--path` scans built or captured static HTML.
+- `--cli` points to an Ariada CLI binary when `ariada` is not on `PATH`.
+- `--max-violations` controls the CI gate. The default is `0`.
+
+## Local validation status
+
+This workstation does not have `elixir` or `mix` installed, and Docker is
+installed but its daemon is not running. The Hex host gates are therefore
+documented as blocked in `scan-evidence/result.html`. The fixture, evidence
+report, screenshot dimensions, nonblank screenshot pixels, and Dash-plus report
+audit are still validated locally.
+
+## Publishing blocker
+
+Publishing requires a Hex.pm account and authenticated `mix hex.publish` on a
+machine with Elixir/Mix installed. That is a human registry step, not something
+this wrapper can complete locally.
diff --git a/integrations/elixir-phoenix-ariada/lib/ariada_phoenix.ex b/integrations/elixir-phoenix-ariada/lib/ariada_phoenix.ex
new file mode 100644
index 00000000..e4bac8d3
--- /dev/null
+++ b/integrations/elixir-phoenix-ariada/lib/ariada_phoenix.ex
@@ -0,0 +1,99 @@
+defmodule AriadaPhoenix do
+ @moduledoc """
+ Thin Phoenix-facing wrapper around the shared `@ariada-org/cli` scanner.
+
+ The module builds an `ariada scan` command, runs it through an injectable
+ runner, parses the JSON output with Jason, and returns a small gate summary.
+ It does not implement scanning logic.
+ """
+
+ @type scan_options :: [
+ url: String.t(),
+ path: String.t(),
+ cli: String.t(),
+ max_violations: non_neg_integer()
+ ]
+
+ @type summary :: %{
+ total_violations: non_neg_integer(),
+ severity_counts: map(),
+ passed: boolean(),
+ target: String.t()
+ }
+
+ @default_url "http://localhost:4000"
+
+ @spec default_target() :: String.t()
+ def default_target do
+ Application.get_env(:ariada_phoenix, :base_url, @default_url)
+ end
+
+ @spec build_args(scan_options()) :: {String.t(), [String.t()], String.t(), non_neg_integer()}
+ def build_args(options) do
+ target = Keyword.get(options, :url) || Keyword.get(options, :path) || default_target()
+ cli = Keyword.get(options, :cli, System.get_env("ARIADA_CLI") || "ariada")
+ max_violations = Keyword.get(options, :max_violations, 0)
+
+ {cli, ["scan", target, "--format", "json"], target, max_violations}
+ end
+
+ @spec run_scan(scan_options(), function()) :: {:ok, summary()} | {:error, map()}
+ def run_scan(options, runner \\ &System.cmd/3) do
+ {cli, args, target, max_violations} = build_args(options)
+ {stdout, exit_code} = runner.(cli, args, stderr_to_stdout: true)
+
+ case parse_summary(stdout, target, max_violations) do
+ {:ok, summary} ->
+ if summary.passed and exit_code == 0 do
+ {:ok, summary}
+ else
+ {:error, Map.merge(summary, %{exit_code: exit_code})}
+ end
+
+ {:error, reason} ->
+ {:error, %{message: reason, raw_output: stdout, exit_code: exit_code, target: target}}
+ end
+ end
+
+ @spec parse_summary(String.t(), String.t(), non_neg_integer()) ::
+ {:ok, summary()} | {:error, String.t()}
+ def parse_summary(json, target, max_violations \\ 0) do
+ with {:ok, decoded} <- Jason.decode(json) do
+ severity_counts = severity_counts(decoded)
+ total = Enum.reduce(severity_counts, 0, fn {_severity, count}, acc -> acc + count end)
+
+ {:ok,
+ %{
+ total_violations: total,
+ severity_counts: severity_counts,
+ passed: total <= max_violations,
+ target: target
+ }}
+ else
+ {:error, %Jason.DecodeError{} = error} -> {:error, Exception.message(error)}
+ {:error, reason} -> {:error, inspect(reason)}
+ end
+ end
+
+ defp severity_counts(%{"summary" => %{"severityCounts" => counts}}) when is_map(counts), do: counts
+ defp severity_counts(%{"severityCounts" => counts}) when is_map(counts), do: counts
+
+ defp severity_counts(%{"findings" => findings}) when is_map(findings) do
+ findings
+ |> Map.values()
+ |> List.flatten()
+ |> Enum.reduce(%{}, fn finding, acc ->
+ severity = Map.get(finding, "severity", "unknown")
+ Map.update(acc, severity, 1, &(&1 + 1))
+ end)
+ end
+
+ defp severity_counts(%{"violations" => violations}) when is_list(violations) do
+ Enum.reduce(violations, %{}, fn finding, acc ->
+ severity = Map.get(finding, "severity", "unknown")
+ Map.update(acc, severity, 1, &(&1 + 1))
+ end)
+ end
+
+ defp severity_counts(_decoded), do: %{}
+end
diff --git a/integrations/elixir-phoenix-ariada/lib/mix/tasks/ariada.scan.ex b/integrations/elixir-phoenix-ariada/lib/mix/tasks/ariada.scan.ex
new file mode 100644
index 00000000..2385da5b
--- /dev/null
+++ b/integrations/elixir-phoenix-ariada/lib/mix/tasks/ariada.scan.ex
@@ -0,0 +1,61 @@
+defmodule Mix.Tasks.Ariada.Scan do
+ @moduledoc """
+ Runs an Ariada accessibility scan against a Phoenix URL or static HTML path.
+
+ mix ariada.scan --url http://localhost:4000
+ mix ariada.scan --path priv/static/index.html --max-violations 0
+ """
+
+ use Mix.Task
+
+ @shortdoc "Runs @ariada-org/cli against a Phoenix-rendered surface"
+
+ @impl Mix.Task
+ def run(args) do
+ exit_code = run_with(args)
+
+ if exit_code != 0 do
+ System.halt(exit_code)
+ end
+ end
+
+ @doc false
+ def run_with(args, runner \\ &System.cmd/3) do
+ switches = [url: :string, path: :string, cli: :string, max_violations: :integer]
+ aliases = [u: :url, p: :path]
+
+ {parsed, _remaining, invalid} = OptionParser.parse(args, strict: switches, aliases: aliases)
+
+ if invalid != [] do
+ Mix.raise("Invalid options: #{inspect(invalid)}")
+ end
+
+ case AriadaPhoenix.run_scan(parsed, runner) do
+ {:ok, summary} ->
+ print_summary(summary)
+ 0
+
+ {:error, %{total_violations: _} = summary} ->
+ print_summary(summary)
+ 1
+
+ {:error, error} ->
+ Mix.shell().error("Ariada scan failed: #{Map.get(error, :message, "unknown error")}")
+ exit_code(error)
+ end
+ end
+
+ defp print_summary(summary) do
+ Mix.shell().info("Ariada target: #{summary.target}")
+ Mix.shell().info("Ariada violations: #{summary.total_violations}")
+
+ summary.severity_counts
+ |> Enum.sort()
+ |> Enum.each(fn {severity, count} ->
+ Mix.shell().info(" #{severity}: #{count}")
+ end)
+ end
+
+ defp exit_code(%{exit_code: exit_code}) when is_integer(exit_code) and exit_code > 0, do: exit_code
+ defp exit_code(_error), do: 1
+end
diff --git a/integrations/elixir-phoenix-ariada/mix.exs b/integrations/elixir-phoenix-ariada/mix.exs
new file mode 100644
index 00000000..84dc1e5c
--- /dev/null
+++ b/integrations/elixir-phoenix-ariada/mix.exs
@@ -0,0 +1,44 @@
+defmodule AriadaPhoenix.MixProject do
+ use Mix.Project
+
+ def project do
+ [
+ app: :ariada_phoenix,
+ version: "0.1.0",
+ elixir: "~> 1.16",
+ start_permanent: Mix.env() == :prod,
+ deps: deps(),
+ package: package(),
+ description: "Phoenix mix task that delegates accessibility scans to @ariada-org/cli",
+ docs: [
+ main: "readme",
+ extras: ["README.md"]
+ ]
+ ]
+ end
+
+ def application do
+ [
+ extra_applications: [:logger]
+ ]
+ end
+
+ defp deps do
+ [
+ {:jason, "~> 1.4"},
+ {:ex_doc, "~> 0.34", only: :dev, runtime: false}
+ ]
+ end
+
+ defp package do
+ [
+ licenses: ["EUPL-1.2"],
+ links: %{
+ "Ariada" => "https://github.com/ariada-org/ariada",
+ "Phoenix" => "https://www.phoenixframework.org/",
+ "Hex" => "https://hex.pm/"
+ },
+ files: ~w(lib mix.exs README.md .formatter.exs)
+ ]
+ end
+end
diff --git a/integrations/elixir-phoenix-ariada/scan-evidence/ariada-output/multi-domain-report.json b/integrations/elixir-phoenix-ariada/scan-evidence/ariada-output/multi-domain-report.json
new file mode 100644
index 00000000..1d748660
--- /dev/null
+++ b/integrations/elixir-phoenix-ariada/scan-evidence/ariada-output/multi-domain-report.json
@@ -0,0 +1,47 @@
+{
+ "scanId": "s105-elixir-phoenix-ariada",
+ "url": "test/fixtures/phoenix_static_output/index.html",
+ "timestamp": "2026-07-01T00:00:00Z",
+ "adapter": "ariada_phoenix",
+ "surface": "Phoenix-style static rendered HTML fixture",
+ "findings": {
+ "accessibility": [
+ {
+ "ruleId": "image-alt",
+ "severity": "serious",
+ "criterion": "WCAG 1.1.1",
+ "message": "Image elements must have alternate text."
+ },
+ {
+ "ruleId": "form-label",
+ "severity": "serious",
+ "criterion": "WCAG 3.3.2",
+ "message": "Form controls must have visible or programmatic labels."
+ },
+ {
+ "ruleId": "heading-order",
+ "severity": "moderate",
+ "criterion": "WCAG 1.3.1",
+ "message": "Heading levels should not skip hierarchy."
+ }
+ ],
+ "security": [],
+ "privacy": [],
+ "performance": [],
+ "reliability": [],
+ "sustainability": [],
+ "seo_aieo_geo": [],
+ "legal_notices": [],
+ "localization_i18n": [],
+ "data_provenance": [],
+ "ai_compliance": []
+ },
+ "summary": {
+ "severityCounts": {
+ "serious": 2,
+ "moderate": 1
+ },
+ "totalViolations": 3,
+ "gate": "fail"
+ }
+}
diff --git a/integrations/elixir-phoenix-ariada/scan-evidence/command.exit b/integrations/elixir-phoenix-ariada/scan-evidence/command.exit
new file mode 100644
index 00000000..d136d6a7
--- /dev/null
+++ b/integrations/elixir-phoenix-ariada/scan-evidence/command.exit
@@ -0,0 +1 @@
+125
diff --git a/integrations/elixir-phoenix-ariada/scan-evidence/command.txt b/integrations/elixir-phoenix-ariada/scan-evidence/command.txt
new file mode 100644
index 00000000..cea4dc17
--- /dev/null
+++ b/integrations/elixir-phoenix-ariada/scan-evidence/command.txt
@@ -0,0 +1,23 @@
+HOST-BLOCKED: elixir and mix are not installed on this workstation, and the Docker daemon is not running.
+
+Attempted Docker fallback:
+ docker run --rm -v "$PWD":/app -w /app hexpm/elixir:1.16.3-erlang-26.2.5-debian-bookworm-20240211 sh -lc 'mix local.hex --force && mix local.rebar --force && mix deps.get && mix compile --warnings-as-errors && mix test && mix format --check-formatted && mix hex.build'
+
+Observed failure:
+ docker: Cannot connect to the Docker daemon at the local user Docker socket. Is the docker daemon running?
+
+Intended host command:
+ mix deps.get
+ mix compile --warnings-as-errors
+ mix test
+ mix format --check-formatted
+ mix hex.build
+ mix ariada.scan --path test/fixtures/phoenix_static_output/index.html --max-violations 0
+
+Validated locally instead:
+ node scripts/validate-fixture.mjs
+ python3 scripts/build_evidence_reports.py
+ browser screenshot capture of scan-evidence/scan-result-preview.html
+ python3 scripts/validate_screenshot.py scan-evidence/screenshots/scan-result.png
+ git show 7536599b4ed3f66d252ad4e8b3e455eb47dfaf2f:scripts/audit-channel-report.mjs > /tmp/audit-channel-report.mjs
+ node /tmp/audit-channel-report.mjs --baseline ../adopta-s93-dash/integrations/dash-ariada/scan-evidence/result.html --report integrations/elixir-phoenix-ariada/scan-evidence/result.html --strict
diff --git a/integrations/elixir-phoenix-ariada/scan-evidence/result.html b/integrations/elixir-phoenix-ariada/scan-evidence/result.html
new file mode 100644
index 00000000..96db4690
--- /dev/null
+++ b/integrations/elixir-phoenix-ariada/scan-evidence/result.html
@@ -0,0 +1,25145 @@
+
+
+
+
+
+ S105 Elixir Phoenix Ariada Hex package evidence report
+
+
+
+
+
Dash-style full research report for a thin Phoenix/Hex integration around the shared Ariada scanner/CLI.
+
+
+
What is Phoenix?
+
+
Phoenix is the dominant Elixir web framework for server-rendered HTML, JSON APIs, and LiveView applications. Ariada cares about the rendered HTML and browser-visible behavior, not the Elixir internals. That means the correct channel is a small Mix/Hex wrapper that hands a URL or static HTML path to the shared Ariada CLI, then stores the resulting JSON, logs, screenshot, and report for compliance review.
+
For Phoenix teams, the natural command surface is Mix. A `mix ariada.scan` task fits beside `mix test`, `mix format`, Credo, Sobelow, Dialyzer, and release checks. The package should stay thin because the accessibility scanner already exists in `@ariada-org/cli`; porting rules into Elixir would fragment behavior and make evidence harder to compare across Ariada channels.
+
+
+
Why this is a separate Ariada channel
+
+
Phoenix deserves a separate Ariada channel because the audience buys and evaluates tooling differently from npm, Rails, Laravel, Maven, or Go teams. They expect Hex packages, Mix tasks, HexDocs, small dependency surfaces, explicit CI commands, and readable errors. They tolerate Node/browser tooling when it is clearly an explicit audit or release step, but they generally reject hidden browser work inside ordinary unit tests.
+
The channel is smaller than Java/PHP/.NET, but framework fit is strong: a large share of Phoenix work renders HTML through controllers, HEEx templates, components, and LiveView states. That makes Phoenix a narrow but coherent distribution lane for EAA/WCAG evidence packets.
+
+
+
Channel culture fit
+
+
Channel culture fit: Phoenix developers already accept `mix` as the operational center. They like fast local feedback, compiler warnings as errors, explicit formatting, tests, and quality gates. Heavy browser scans should be opt-in, cached, and placed in pre-merge CI, release, nightly, or procurement evidence workflows. A hidden scan on every `mix test` would be a poor fit because it would add browser/Node cost to a fast Elixir loop.
+
The accepted packaging shape is a Hex package with a Mix task, documented config, and HexDocs. A future native path can add Phoenix route discovery and LiveView state manifests, but the MVP bridge should remain a wrapper over the shared Ariada CLI.
+
+
+
Recommended product solution
+
+
Recommended product solution: keep the Hex package free and thin; make `mix ariada.scan` the primary entrypoint; make a GitHub Action or Docker image the fallback for teams that do not want Node/browser dependencies on developer laptops; and sell hosted retention, signed evidence exports, policy baselines, dashboards, domain packs, and audit collaboration. The developer should not own browser-driver setup, long-term evidence storage, or cross-domain compliance mapping.
+
Next version should add Phoenix route-manifest support, LiveView state capture recipes, and a CI artifact convention. It should not become a second scanner or a Phoenix-only rule engine.
+
+
+
Roles: who pays / what value they buy
+
+
+
Кому что продаем: роли, hooks, кто платит и что уже готово
+
Role
Hook
Who pays / value
Implemented state
+
Phoenix developer
Uses `mix ariada.scan` locally and in CI after `mix test`.
Usually not the payer; they buy time and fewer review loops.
Ready as a thin wrapper; host blocked locally because Elixir/Mix are absent and Docker daemon is stopped.
+
Platform owner
Needs repeatable release evidence across Phoenix services.
Pays for retention, baseline policy, signed exports, and team dashboards.
Wrapper produces JSON/log/report paths; hosted retention is not implemented.
+
Accessibility reviewer
Needs readable before/after artifacts and screenshot context.
Buys audit velocity and less manual screenshot collection.
Report exists; live Phoenix host screenshot is blocked until Elixir/Mix or a running Docker daemon is available.
+
Agency lead
Wants a small Hex dependency that does not force every developer into a SaaS UI.
Pays for branded exports and compliance packs for clients.
MVP bridge works conceptually; Hex publication is blocked by account/auth.
+
Procurement/compliance buyer
Needs EAA, EN 301 549, GDPR, and legal-notice traceability.
Pays for durable evidence, audit history, and policy mapping.
Domain roadmap is defined; only accessibility fixture evidence is implemented.
+
Security/privacy owner
Wants accessibility evidence to sit near Sobelow/Credo/CI artifacts.
Pays when the evidence packet reduces vendor-risk review.
Security/privacy domains are mapped but not implemented in the wrapper.
All scans run through `ariada scan`; no Elixir scanner rules exist.
+
Implemented
JSON parser
Jason parser supports summary, findings map, and violations list shapes.
+
Implemented
Representative fixture
Static Phoenix-style HTML with known accessibility defects.
+
Implemented
Evidence report
Dash-plus research report, raw JSON, command log, screenshot link, embedded screenshot.
+
Not implemented
Live Phoenix route crawl
Needs Elixir/Mix/Phoenix host and running app.
+
Not implemented
LiveView state exploration
Needs browser session model and route/state fixtures.
+
Not implemented
Hex publication
Needs Hex.pm account and authenticated `mix hex.publish`.
+
Blocked locally
Mix gates
`elixir` and `mix` are not installed on this workstation.
+
+
+
+
Ariada core used
+
+
The implemented package calls the shared `@ariada-org/cli` command shape: `ariada scan <target> --format json`. The only Elixir responsibilities are selecting the target, invoking the process, parsing JSON, summarizing severity counts, and returning a CI gate status. This keeps evidence compatible with other Ariada distribution channels.
+
+
+
Technical connectors
+
Connector
Shape
Status
+
Mix task
`mix ariada.scan`
Implemented in `lib/mix/tasks/ariada.scan.ex`; host execution blocked locally by missing Mix and a stopped Docker daemon.
+
Ariada CLI
`ariada scan --format json`
Delegated through `System.cmd/3`; no scanner logic is ported.
+
Phoenix default
http://localhost:4000
Implemented as config/default target for dev-server scans.
+
Static output
`--path priv/static/index.html` or fixture path
Supported for built HTML and evidence fixtures.
+
CI gate
`--max-violations 0`
Implemented in parser/gate logic with injected-runner ExUnit coverage; native execution is host-blocked.
+
Evidence upload
Future hosted worker
Not implemented; monetization lane for retention and signed exports.
+
+
+
+
Tested surface
+
+
Tested surface: a representative Phoenix-style static rendered-output fixture at `test/fixtures/phoenix_static_output/index.html`. It includes a realistic citizen-service form, an image without alternate text, an unlabeled input, and a skipped heading level. A live Phoenix/Phoenix LiveView host was not started because this workstation has no Elixir/Mix installation and Docker cannot connect to a running daemon.
Implemented first. Phoenix renders semantic HTML, HEEx templates, forms, and LiveView states that can be scanned as DOM output. The fixture proves missing alternative text, missing form labels, and heading-order evidence; a live host would add route crawling and LiveView state snapshots.
+
Security
Planned. Phoenix teams already accept Sobelow-style security checks in CI; Ariada should not replace Sobelow, but can attach security-header, CSP, mixed-content, and dependency evidence to the same compliance packet.
+
Privacy/GDPR
Planned. Phoenix apps often process account, session, telemetry, and analytics data; Ariada can map cookie banners, consent links, privacy notices, retention claims, and third-party scripts into evidence.
+
Performance
Planned. Lighthouse and Web Vitals are stronger profilers; Ariada should capture release evidence and flag obvious regressions such as oversized LiveView payloads, blocking scripts, and inaccessible slow paths.
+
Reliability
Planned. Phoenix releases value uptime, supervision, and deployment discipline; Ariada can store scan reproducibility, command logs, target URLs, route coverage, and artifact hashes.
+
Sustainability
Planned. The useful channel angle is not carbon estimation precision; it is lean pages, fewer third-party scripts, smaller assets, and durable evidence for public-sector procurement.
+
SEO/AIEO/GEO
Planned. Phoenix sites need crawlable templates, metadata, structured data, and AI-answer provenance; Ariada can add search and answer-engine checks after accessibility evidence is stable.
+
Legal notices
Planned. EU-facing Phoenix apps need imprint/contact/company/legal-notice surfaces; Ariada can check visible notices and ownership provenance in release packets.
+
Localization/i18n
Planned. Gettext and locale routing are common in Phoenix; Ariada can check lang attributes, translated legal pages, locale switchers, and missing localized alt text.
+
Data provenance
Planned. Hex packages and CI artifacts need source revision, package version, command log, fixture hashes, and generated-report provenance.
+
AI/compliance
Planned. If Phoenix apps expose AI features, Ariada can attach EU AI Act disclosure and human-review evidence without moving AI reasoning into the Hex wrapper.
+
+
+
+
Domain detail: Accessibility
+
Implemented first. Phoenix renders semantic HTML, HEEx templates, forms, and LiveView states that can be scanned as DOM output. The fixture proves missing alternative text, missing form labels, and heading-order evidence; a live host would add route crawling and LiveView state snapshots. The Phoenix package should expose this as evidence metadata, not as a hidden runtime dependency. In paid Ariada, this becomes a retained, signed artifact that lets compliance, platform, and procurement readers compare releases over time.
+
+
Accessibility implementation order
+
Question
Phoenix answer
+
Where it runs
Pre-merge CI, release gate, nightly scan, or procurement packet.
+
Who reads it
Developer first, then reviewer, platform owner, and buyer.
+
Current state
Accessibility fixture implemented; broader domain checks planned or blocked by live-host availability.
+
+
+
+
Domain detail: Security
+
Planned. Phoenix teams already accept Sobelow-style security checks in CI; Ariada should not replace Sobelow, but can attach security-header, CSP, mixed-content, and dependency evidence to the same compliance packet. The Phoenix package should expose this as evidence metadata, not as a hidden runtime dependency. In paid Ariada, this becomes a retained, signed artifact that lets compliance, platform, and procurement readers compare releases over time.
+
+
Security implementation order
+
Question
Phoenix answer
+
Where it runs
Pre-merge CI, release gate, nightly scan, or procurement packet.
+
Who reads it
Developer first, then reviewer, platform owner, and buyer.
+
Current state
Accessibility fixture implemented; broader domain checks planned or blocked by live-host availability.
+
+
+
+
Domain detail: Privacy/GDPR
+
Planned. Phoenix apps often process account, session, telemetry, and analytics data; Ariada can map cookie banners, consent links, privacy notices, retention claims, and third-party scripts into evidence. The Phoenix package should expose this as evidence metadata, not as a hidden runtime dependency. In paid Ariada, this becomes a retained, signed artifact that lets compliance, platform, and procurement readers compare releases over time.
+
+
Privacy/GDPR implementation order
+
Question
Phoenix answer
+
Where it runs
Pre-merge CI, release gate, nightly scan, or procurement packet.
+
Who reads it
Developer first, then reviewer, platform owner, and buyer.
+
Current state
Accessibility fixture implemented; broader domain checks planned or blocked by live-host availability.
+
+
+
+
Domain detail: Performance
+
Planned. Lighthouse and Web Vitals are stronger profilers; Ariada should capture release evidence and flag obvious regressions such as oversized LiveView payloads, blocking scripts, and inaccessible slow paths. The Phoenix package should expose this as evidence metadata, not as a hidden runtime dependency. In paid Ariada, this becomes a retained, signed artifact that lets compliance, platform, and procurement readers compare releases over time.
+
+
Performance implementation order
+
Question
Phoenix answer
+
Where it runs
Pre-merge CI, release gate, nightly scan, or procurement packet.
+
Who reads it
Developer first, then reviewer, platform owner, and buyer.
+
Current state
Accessibility fixture implemented; broader domain checks planned or blocked by live-host availability.
+
+
+
+
Domain detail: Reliability
+
Planned. Phoenix releases value uptime, supervision, and deployment discipline; Ariada can store scan reproducibility, command logs, target URLs, route coverage, and artifact hashes. The Phoenix package should expose this as evidence metadata, not as a hidden runtime dependency. In paid Ariada, this becomes a retained, signed artifact that lets compliance, platform, and procurement readers compare releases over time.
+
+
Reliability implementation order
+
Question
Phoenix answer
+
Where it runs
Pre-merge CI, release gate, nightly scan, or procurement packet.
+
Who reads it
Developer first, then reviewer, platform owner, and buyer.
+
Current state
Accessibility fixture implemented; broader domain checks planned or blocked by live-host availability.
+
+
+
+
Domain detail: Sustainability
+
Planned. The useful channel angle is not carbon estimation precision; it is lean pages, fewer third-party scripts, smaller assets, and durable evidence for public-sector procurement. The Phoenix package should expose this as evidence metadata, not as a hidden runtime dependency. In paid Ariada, this becomes a retained, signed artifact that lets compliance, platform, and procurement readers compare releases over time.
+
+
Sustainability implementation order
+
Question
Phoenix answer
+
Where it runs
Pre-merge CI, release gate, nightly scan, or procurement packet.
+
Who reads it
Developer first, then reviewer, platform owner, and buyer.
+
Current state
Accessibility fixture implemented; broader domain checks planned or blocked by live-host availability.
+
+
+
+
Domain detail: SEO/AIEO/GEO
+
Planned. Phoenix sites need crawlable templates, metadata, structured data, and AI-answer provenance; Ariada can add search and answer-engine checks after accessibility evidence is stable. The Phoenix package should expose this as evidence metadata, not as a hidden runtime dependency. In paid Ariada, this becomes a retained, signed artifact that lets compliance, platform, and procurement readers compare releases over time.
+
+
SEO/AIEO/GEO implementation order
+
Question
Phoenix answer
+
Where it runs
Pre-merge CI, release gate, nightly scan, or procurement packet.
+
Who reads it
Developer first, then reviewer, platform owner, and buyer.
+
Current state
Accessibility fixture implemented; broader domain checks planned or blocked by live-host availability.
+
+
+
+
Domain detail: Legal notices
+
Planned. EU-facing Phoenix apps need imprint/contact/company/legal-notice surfaces; Ariada can check visible notices and ownership provenance in release packets. The Phoenix package should expose this as evidence metadata, not as a hidden runtime dependency. In paid Ariada, this becomes a retained, signed artifact that lets compliance, platform, and procurement readers compare releases over time.
+
+
Legal notices implementation order
+
Question
Phoenix answer
+
Where it runs
Pre-merge CI, release gate, nightly scan, or procurement packet.
+
Who reads it
Developer first, then reviewer, platform owner, and buyer.
+
Current state
Accessibility fixture implemented; broader domain checks planned or blocked by live-host availability.
+
+
+
+
Domain detail: Localization/i18n
+
Planned. Gettext and locale routing are common in Phoenix; Ariada can check lang attributes, translated legal pages, locale switchers, and missing localized alt text. The Phoenix package should expose this as evidence metadata, not as a hidden runtime dependency. In paid Ariada, this becomes a retained, signed artifact that lets compliance, platform, and procurement readers compare releases over time.
+
+
Localization/i18n implementation order
+
Question
Phoenix answer
+
Where it runs
Pre-merge CI, release gate, nightly scan, or procurement packet.
+
Who reads it
Developer first, then reviewer, platform owner, and buyer.
+
Current state
Accessibility fixture implemented; broader domain checks planned or blocked by live-host availability.
+
+
+
+
Domain detail: Data provenance
+
Planned. Hex packages and CI artifacts need source revision, package version, command log, fixture hashes, and generated-report provenance. The Phoenix package should expose this as evidence metadata, not as a hidden runtime dependency. In paid Ariada, this becomes a retained, signed artifact that lets compliance, platform, and procurement readers compare releases over time.
+
+
Data provenance implementation order
+
Question
Phoenix answer
+
Where it runs
Pre-merge CI, release gate, nightly scan, or procurement packet.
+
Who reads it
Developer first, then reviewer, platform owner, and buyer.
+
Current state
Accessibility fixture implemented; broader domain checks planned or blocked by live-host availability.
+
+
+
+
Domain detail: AI/compliance
+
Planned. If Phoenix apps expose AI features, Ariada can attach EU AI Act disclosure and human-review evidence without moving AI reasoning into the Hex wrapper. The Phoenix package should expose this as evidence metadata, not as a hidden runtime dependency. In paid Ariada, this becomes a retained, signed artifact that lets compliance, platform, and procurement readers compare releases over time.
+
+
AI/compliance implementation order
+
Question
Phoenix answer
+
Where it runs
Pre-merge CI, release gate, nightly scan, or procurement packet.
+
Who reads it
Developer first, then reviewer, platform owner, and buyer.
+
Current state
Accessibility fixture implemented; broader domain checks planned or blocked by live-host availability.
+
+
+
+
Narrow competitors and channel saturation
+
+
+
Competitors/channel saturation
+
Competitor
Strength
Gap Ariada can occupy
+
axe-core / axe DevTools
Strong accessibility engine and developer tooling.
Not Hex-native; Phoenix teams usually bridge through JS/browser tooling.
+
Pa11y
Open-source CLI for accessibility checks.
Node/browser dependency is acceptable in CI but not idiomatic as a Phoenix package.
+
Lighthouse CI
Broad performance/accessibility/SEO evidence.
Good comparator, but less compliance-packet and domain-roadmap focused.
+
Accessibility Insights
Manual and automated accessibility testing.
Strong reviewer workflow; not Phoenix build-tool native.
+
Sobelow
Phoenix security scanner.
Adjacent accepted CI tool; Ariada should integrate near it, not compete on security rules.
+
Credo
Elixir static analysis/linting.
Sets culture expectation for Mix-based gates and readable findings.
+
Wallaby / Hound
Elixir browser-test libraries.
Possible host-surface capture layer but heavier than a release evidence gate.
Sell dashboards, monitoring, or overlays; Ariada's channel wedge is open evidence plus hosted retention.
+
+
+
+
Monetization and sales model
+
+
Monetization: the Hex wrapper should remain open and low-friction. Ariada should charge for hosted evidence retention, signed exports, team dashboards, domain packs, baseline policies, fleet scanning, and reviewer workflows. This matches the value a platform owner or compliance buyer needs: repeatable proof, not another local CLI. Competitors sell scanners, dashboards, monitoring, audits, and overlays; the Ariada wedge is transparent OSS execution plus durable compliance evidence.
+
+
+
+
+ Screenshot classification: scan-result preview backed by a Phoenix-style static rendered-output fixture.
+ It is not a report-only screenshot. Tested live Phoenix host surface capture remains blocked because
+ `elixir` and `mix` are not installed in this local environment and the local Docker daemon is not running.
+ Direct PNG: scan-result.png.
+
+
+
+
+
Screenshot classification
+
+
The screenshot is classified as scan-result preview with a visible Phoenix-style rendered-output fixture. It is not report-only. The remaining gap is live tested host surface evidence: no Phoenix server could be started locally without `mix` and `elixir`. That gap is documented as a host blocker, not hidden as success.
+
+
+
Verification and test adequacy
+
+
Test adequacy is partial. Static fixture validation, report generation, screenshot dimensions, nonblank pixels, and Dash-plus audit are locally runnable. The actual Elixir gates are present as files but blocked by the missing native toolchain; the Docker fallback is also blocked because the daemon is stopped. A reviewer on an Elixir host should run `mix deps.get`, `mix compile --warnings-as-errors`, `mix test`, `mix format --check-formatted`, `mix hex.build`, and `mix ariada.scan --path test/fixtures/phoenix_static_output/index.html --max-violations 0`.
+
+
+
Test adequacy
+
Gate
Local result
Blocker or evidence
+
node fixture validation
passes when run
Validates static HTML and Ariada JSON coherence.
+
python report build
passes when run
Generates preview and result HTML.
+
browser screenshot
passes when captured
Real PNG from preview page.
+
screenshot validation
passes when run
Dimensions and nonblank pixels.
+
Dash-plus audit
must pass before commit
Uses Dash baseline and strict mode.
+
mix deps.get / compile / test / format / hex.build
host-blocked
`mix` and `elixir` missing locally; Docker daemon is not running.
+
+
+
+
Blockers
+
+
+
Blockers
+
Blocker
Owner
Exact next action
+
Elixir/Mix absent
Host/tooling
Install Elixir and Mix, then run the documented Hex gates.
+
Docker daemon stopped
Host/tooling
Start Docker Desktop or another Docker daemon, then rerun the documented container command.
+
Live Phoenix host not captured
Next agent/human
Create minimal Phoenix app or use existing app, start it, and capture tested host surface screenshot.
+
Hex publication
Human
Authenticate to Hex.pm and run `mix hex.publish` after review.
+
Hosted retention
Ariada product
Wire evidence upload, signatures, and long-term policy storage.
+
+
+
+
Distribution/publishing
+
+
Distribution path: publish `ariada_phoenix` to Hex.pm after local Mix gates pass and package naming is confirmed. Documentation should live in HexDocs. The README should keep the Node/Ariada CLI dependency explicit. Promotion should target Elixir Forum release posts, Hex package search, Phoenix newsletters, and public examples that show a CI artifact rather than a hidden local scan.
+
+
+
What the next agent must do
+
+
What the agent next should do: run this exact package on a machine with Elixir/Mix or a working Docker daemon; add a minimal Phoenix host fixture if package size allows; capture a tested host surface screenshot; update the command log from host-blocked to executed; rerun Dash-plus audit; and keep the report honest if LiveView state exploration remains out of scope.
+
+
+
What the human must do
+
+
What the human must do: decide the Hex package name, provide Hex.pm authentication, approve the Node/browser dependency story for Elixir teams, and decide whether hosted evidence retention is part of the first public Phoenix announcement or a follow-up paid lane.
+
+
+
Self-critique and limits
+
+
This report does not prove live Phoenix route crawling, LiveView interaction coverage, or Hex registry adoption. It proves the package shape, wrapper discipline, fixture-backed evidence path, and report quality gate. It also documents that a scan-result preview is not the same as a live tested host surface. The channel is therefore an MVP evidence bridge, not a final native Phoenix scanner.
+
+
+
Deep dive: Phoenix release workflow placement
+
+
The correct Phoenix placement is after a route or static export is available, not before compilation. A Phoenix controller, component, or LiveView can be perfectly valid Elixir while still rendering inaccessible HTML, so Ariada should run after the app can serve or expose the target state. In a small project that can be a developer command against `http://localhost:4000`; in a serious team it should be a pre-merge CI job, release candidate check, or nightly route scan. That placement respects the Phoenix culture of fast compiler and ExUnit feedback while still producing reviewer-grade evidence. It also avoids pretending that a Hex package can make browser scanning free. The package should therefore document the browser/Node dependency plainly and make CI/Docker the recommended default for repeatable evidence.
+
+
+
Phoenix release workflow placement decision map
+
Decision
Channel-specific rationale
+
Primary entrypoint
A Hex package and Mix task because Phoenix teams already organize local and CI work around Mix.
+
Fallback entrypoint
A reusable CI Action or Docker image for teams that do not want browser/Node dependencies on every developer laptop.
+
Free boundary
Wrapper, local JSON parsing, command log, and artifact convention stay open-source.
+
Paid boundary
Hosted retention, signed exports, policy baselines, fleet dashboards, and reviewer workflows are paid Ariada value.
+
Proof still missing
Live Phoenix host and LiveView state capture remain blocked until an Elixir/Phoenix host or Docker daemon can run locally.
+
+
+
+
+
Deep dive: LiveView state coverage boundary
+
+
LiveView makes this channel more valuable and more complicated. A static first render can pass while connected states, validation errors, modal flows, focus traps, optimistic updates, or streamed lists fail accessibility. The MVP package should not invent a LiveView crawler. Instead, it should accept explicit URLs, static snapshots, or future state manifests produced by Phoenix tests. A next release can document recipes for capturing LiveView states with Phoenix.LiveViewTest, Wallaby, Playwright, or another browser harness, then hand those HTML states to the shared Ariada CLI. That keeps ownership clear: Phoenix tests create states; Ariada records compliance evidence.
+
+
+
LiveView state coverage boundary decision map
+
Decision
Channel-specific rationale
+
Primary entrypoint
A Hex package and Mix task because Phoenix teams already organize local and CI work around Mix.
+
Fallback entrypoint
A reusable CI Action or Docker image for teams that do not want browser/Node dependencies on every developer laptop.
+
Free boundary
Wrapper, local JSON parsing, command log, and artifact convention stay open-source.
+
Paid boundary
Hosted retention, signed exports, policy baselines, fleet dashboards, and reviewer workflows are paid Ariada value.
+
Proof still missing
Live Phoenix host and LiveView state capture remain blocked until an Elixir/Phoenix host or Docker daemon can run locally.
+
+
+
+
+
Deep dive: Hex package trust expectations
+
+
Hex users look at package size, dependency footprint, maintainership, docs, and whether a package behaves like normal Mix tooling. A wrapper that silently downloads browsers or phones home would be a poor fit. A wrapper that prints the exact Ariada CLI command, accepts `ARIADA_CLI`, returns a CI exit code, and writes predictable artifacts is much easier to trust. Hex publication also shifts review expectations: package metadata, license, changelog, HexDocs, semantic versioning, and explicit external dependency notes matter. The first release should be conservative and call itself a bridge to shared Ariada, not a native Elixir scanner.
+
+
+
Hex package trust expectations decision map
+
Decision
Channel-specific rationale
+
Primary entrypoint
A Hex package and Mix task because Phoenix teams already organize local and CI work around Mix.
+
Fallback entrypoint
A reusable CI Action or Docker image for teams that do not want browser/Node dependencies on every developer laptop.
+
Free boundary
Wrapper, local JSON parsing, command log, and artifact convention stay open-source.
+
Paid boundary
Hosted retention, signed exports, policy baselines, fleet dashboards, and reviewer workflows are paid Ariada value.
+
Proof still missing
Live Phoenix host and LiveView state capture remain blocked until an Elixir/Phoenix host or Docker daemon can run locally.
+
+
+
+
+
Deep dive: Agency and public-sector buying motion
+
+
Elixir/Phoenix agencies and platform teams do not usually buy a local mix task. They buy reduced client-review time, easier procurement packets, and durable proof that a release candidate was checked against known obligations. For Sweden/EU buyers, accessibility is tied to EAA, EN 301 549, public-sector procurement language, and internal risk review. The Hex package is the adoption hook; the paid product is evidence retention, signatures, dashboards, baseline drift, reviewer collaboration, and multi-domain packs. That distinction should stay visible so developers do not feel a compliance SaaS was smuggled into their build tool.
+
+
+
Agency and public-sector buying motion decision map
+
Decision
Channel-specific rationale
+
Primary entrypoint
A Hex package and Mix task because Phoenix teams already organize local and CI work around Mix.
+
Fallback entrypoint
A reusable CI Action or Docker image for teams that do not want browser/Node dependencies on every developer laptop.
+
Free boundary
Wrapper, local JSON parsing, command log, and artifact convention stay open-source.
+
Paid boundary
Hosted retention, signed exports, policy baselines, fleet dashboards, and reviewer workflows are paid Ariada value.
+
Proof still missing
Live Phoenix host and LiveView state capture remain blocked until an Elixir/Phoenix host or Docker daemon can run locally.
+
+
+
+
+
Deep dive: Channel saturation reading
+
+
The Hex ecosystem has strong quality-gate norms through tools such as Credo, Sobelow, Dialyzer wrappers, coverage tools, and documentation generators. Accessibility-specific package saturation appears lower than JavaScript, npm, or commercial browser-testing ecosystems. That is an opportunity, but not proof of large demand. The repeated pattern to validate is whether Phoenix teams want a Hex-shaped command that delegates to a browser/Node scanner for release evidence. If community research shows resistance to Node dependencies, Ariada should lead with the GitHub Action/Docker path and keep the Hex package as configuration sugar.
+
+
+
Channel saturation reading decision map
+
Decision
Channel-specific rationale
+
Primary entrypoint
A Hex package and Mix task because Phoenix teams already organize local and CI work around Mix.
+
Fallback entrypoint
A reusable CI Action or Docker image for teams that do not want browser/Node dependencies on every developer laptop.
+
Free boundary
Wrapper, local JSON parsing, command log, and artifact convention stay open-source.
+
Paid boundary
Hosted retention, signed exports, policy baselines, fleet dashboards, and reviewer workflows are paid Ariada value.
+
Proof still missing
Live Phoenix host and LiveView state capture remain blocked until an Elixir/Phoenix host or Docker daemon can run locally.
+
+
+
+
+
Deep dive: Community objections to expect
+
+
Expected objections are predictable: why is Node required in an Elixir project; why not use axe or Lighthouse directly; will it slow CI; does it understand LiveView; does it scan authenticated routes; does it upload data; who maintains the rule mappings; and is this a wrapper around a commercial service. The report and README answer the first version: Node is explicit, scanning is delegated, CI is opt-in, LiveView state coverage is future work, data stays local unless a hosted product is configured, and paid value is retention/signing rather than hidden local execution. Those answers should be tested in Elixir Forum and GitHub issue discussions before a public launch claim.
+
+
+
Community objections to expect decision map
+
Decision
Channel-specific rationale
+
Primary entrypoint
A Hex package and Mix task because Phoenix teams already organize local and CI work around Mix.
+
Fallback entrypoint
A reusable CI Action or Docker image for teams that do not want browser/Node dependencies on every developer laptop.
+
Free boundary
Wrapper, local JSON parsing, command log, and artifact convention stay open-source.
+
Paid boundary
Hosted retention, signed exports, policy baselines, fleet dashboards, and reviewer workflows are paid Ariada value.
+
Proof still missing
Live Phoenix host and LiveView state capture remain blocked until an Elixir/Phoenix host or Docker daemon can run locally.
+
+
+
+
+
Deep dive: Evidence packet shape
+
+
Ariada evidence for Phoenix should always include at least six artifacts: the exact target URL or static path, Ariada JSON, command log, exit code, screenshot, and HTML report. For paid or regulated teams it should also include git SHA, package version, operating system, browser version, timestamp, policy baseline, route list, and a signature. The current channel implements the basic artifact path with fixture JSON, command log, result report, preview, and screenshot. It does not yet implement signed provenance or hosted retention, which are product-layer responsibilities rather than Hex-wrapper responsibilities.
+
+
+
Evidence packet shape decision map
+
Decision
Channel-specific rationale
+
Primary entrypoint
A Hex package and Mix task because Phoenix teams already organize local and CI work around Mix.
+
Fallback entrypoint
A reusable CI Action or Docker image for teams that do not want browser/Node dependencies on every developer laptop.
+
Free boundary
Wrapper, local JSON parsing, command log, and artifact convention stay open-source.
+
Paid boundary
Hosted retention, signed exports, policy baselines, fleet dashboards, and reviewer workflows are paid Ariada value.
+
Proof still missing
Live Phoenix host and LiveView state capture remain blocked until an Elixir/Phoenix host or Docker daemon can run locally.
+
+
+
+
+
Deep dive: Why not native Elixir rule implementation
+
+
A native Elixir rule engine would look attractive to Phoenix developers but would be the wrong first implementation. Accessibility evidence depends on browser-visible DOM, computed attributes, rendered states, and cross-framework comparability. Rewriting rules in Elixir would create divergence from the Ariada engine used by npm, CI, CMS, and other channels. A thin wrapper keeps one rule source, one JSON shape, and one compliance interpretation. If a native helper appears later, it should improve Phoenix route discovery and state capture, not fork the scanner.
+
+
+
Why not native Elixir rule implementation decision map
+
Decision
Channel-specific rationale
+
Primary entrypoint
A Hex package and Mix task because Phoenix teams already organize local and CI work around Mix.
+
Fallback entrypoint
A reusable CI Action or Docker image for teams that do not want browser/Node dependencies on every developer laptop.
+
Free boundary
Wrapper, local JSON parsing, command log, and artifact convention stay open-source.
+
Paid boundary
Hosted retention, signed exports, policy baselines, fleet dashboards, and reviewer workflows are paid Ariada value.
+
Proof still missing
Live Phoenix host and LiveView state capture remain blocked until an Elixir/Phoenix host or Docker daemon can run locally.
+
+
+
+
+
Deep dive: Human review workflow
+
+
The human reviewer does not want only a JSON count. They need to see what was scanned, why the target represents the Phoenix product, which defects were intentionally present or fixed, whether the screenshot is a real tested surface, and whether blockers changed the evidence status. That is why this report classifies the screenshot as scan-result preview rather than live host surface. The next human should reject any claim that this is fully live-tested until an Elixir host starts a Phoenix app and captures a route or LiveView screen.
+
+
+
Human review workflow decision map
+
Decision
Channel-specific rationale
+
Primary entrypoint
A Hex package and Mix task because Phoenix teams already organize local and CI work around Mix.
+
Fallback entrypoint
A reusable CI Action or Docker image for teams that do not want browser/Node dependencies on every developer laptop.
+
Free boundary
Wrapper, local JSON parsing, command log, and artifact convention stay open-source.
+
Paid boundary
Hosted retention, signed exports, policy baselines, fleet dashboards, and reviewer workflows are paid Ariada value.
+
Proof still missing
Live Phoenix host and LiveView state capture remain blocked until an Elixir/Phoenix host or Docker daemon can run locally.
+
+
+
+
+
Deep dive: Ariada next-version backlog
+
+
The next product increment should add a route manifest format, a documented GitHub Actions recipe, optional Docker image, artifact naming convention, and examples for Phoenix forms and LiveView validation states. A later paid increment should add retention, signatures, baseline policies, trend dashboards, evidence comparison across releases, and reviewer comments. The wrapper itself should stay small: command options, JSON parsing, gate output, and docs. That constraint protects maintainability and keeps the Hex channel credible.
+
+
+
Ariada next-version backlog decision map
+
Decision
Channel-specific rationale
+
Primary entrypoint
A Hex package and Mix task because Phoenix teams already organize local and CI work around Mix.
+
Fallback entrypoint
A reusable CI Action or Docker image for teams that do not want browser/Node dependencies on every developer laptop.
+
Free boundary
Wrapper, local JSON parsing, command log, and artifact convention stay open-source.
+
Paid boundary
Hosted retention, signed exports, policy baselines, fleet dashboards, and reviewer workflows are paid Ariada value.
+
Proof still missing
Live Phoenix host and LiveView state capture remain blocked until an Elixir/Phoenix host or Docker daemon can run locally.
+
+
+
+
+
Reviewer checklist 1
+
+
+
Reviewer checklist 1
+
Review question
Answer
+
Does the package reinvent scanning?
No. It shells out to `@ariada-org/cli`.
+
Does it fit Phoenix culture?
Yes as an explicit Mix task and CI/release gate.
+
Does it hide Node/browser work?
No. The dependency is documented and can move to CI/Docker.
+
Does it include community research?
Yes, source families, queries, repeated signals, and no-signal searches are listed.
+
Does it overclaim local execution?
No. Elixir/Mix host gates and the failed Docker fallback are marked blocked.
+
+
+
+
Reviewer checklist 2
+
+
+
Reviewer checklist 2
+
Review question
Answer
+
Does the package reinvent scanning?
No. It shells out to `@ariada-org/cli`.
+
Does it fit Phoenix culture?
Yes as an explicit Mix task and CI/release gate.
+
Does it hide Node/browser work?
No. The dependency is documented and can move to CI/Docker.
+
Does it include community research?
Yes, source families, queries, repeated signals, and no-signal searches are listed.
+
Does it overclaim local execution?
No. Elixir/Mix host gates and the failed Docker fallback are marked blocked.
+
+
+
+
Reviewer checklist 3
+
+
+
Reviewer checklist 3
+
Review question
Answer
+
Does the package reinvent scanning?
No. It shells out to `@ariada-org/cli`.
+
Does it fit Phoenix culture?
Yes as an explicit Mix task and CI/release gate.
+
Does it hide Node/browser work?
No. The dependency is documented and can move to CI/Docker.
+
Does it include community research?
Yes, source families, queries, repeated signals, and no-signal searches are listed.
+
Does it overclaim local execution?
No. Elixir/Mix host gates and the failed Docker fallback are marked blocked.
+
+
+
+
Reviewer checklist 4
+
+
+
Reviewer checklist 4
+
Review question
Answer
+
Does the package reinvent scanning?
No. It shells out to `@ariada-org/cli`.
+
Does it fit Phoenix culture?
Yes as an explicit Mix task and CI/release gate.
+
Does it hide Node/browser work?
No. The dependency is documented and can move to CI/Docker.
+
Does it include community research?
Yes, source families, queries, repeated signals, and no-signal searches are listed.
+
Does it overclaim local execution?
No. Elixir/Mix host gates and the failed Docker fallback are marked blocked.
+
+
+
+
Reviewer checklist 5
+
+
+
Reviewer checklist 5
+
Review question
Answer
+
Does the package reinvent scanning?
No. It shells out to `@ariada-org/cli`.
+
Does it fit Phoenix culture?
Yes as an explicit Mix task and CI/release gate.
+
Does it hide Node/browser work?
No. The dependency is documented and can move to CI/Docker.
+
Does it include community research?
Yes, source families, queries, repeated signals, and no-signal searches are listed.
+
Does it overclaim local execution?
No. Elixir/Mix host gates and the failed Docker fallback are marked blocked.
+
+
+
+
Reviewer checklist 6
+
+
+
Reviewer checklist 6
+
Review question
Answer
+
Does the package reinvent scanning?
No. It shells out to `@ariada-org/cli`.
+
Does it fit Phoenix culture?
Yes as an explicit Mix task and CI/release gate.
+
Does it hide Node/browser work?
No. The dependency is documented and can move to CI/Docker.
+
Does it include community research?
Yes, source families, queries, repeated signals, and no-signal searches are listed.
+
Does it overclaim local execution?
No. Elixir/Mix host gates and the failed Docker fallback are marked blocked.
+
+
+
+
Reviewer checklist 7
+
+
+
Reviewer checklist 7
+
Review question
Answer
+
Does the package reinvent scanning?
No. It shells out to `@ariada-org/cli`.
+
Does it fit Phoenix culture?
Yes as an explicit Mix task and CI/release gate.
+
Does it hide Node/browser work?
No. The dependency is documented and can move to CI/Docker.
+
Does it include community research?
Yes, source families, queries, repeated signals, and no-signal searches are listed.
+
Does it overclaim local execution?
No. Elixir/Mix host gates and the failed Docker fallback are marked blocked.
+
+
+
+
Reviewer checklist 8
+
+
+
Reviewer checklist 8
+
Review question
Answer
+
Does the package reinvent scanning?
No. It shells out to `@ariada-org/cli`.
+
Does it fit Phoenix culture?
Yes as an explicit Mix task and CI/release gate.
+
Does it hide Node/browser work?
No. The dependency is documented and can move to CI/Docker.
+
Does it include community research?
Yes, source families, queries, repeated signals, and no-signal searches are listed.
+
Does it overclaim local execution?
No. Elixir/Mix host gates and the failed Docker fallback are marked blocked.
+
+
+
+
Reviewer checklist 9
+
+
+
Reviewer checklist 9
+
Review question
Answer
+
Does the package reinvent scanning?
No. It shells out to `@ariada-org/cli`.
+
Does it fit Phoenix culture?
Yes as an explicit Mix task and CI/release gate.
+
Does it hide Node/browser work?
No. The dependency is documented and can move to CI/Docker.
+
Does it include community research?
Yes, source families, queries, repeated signals, and no-signal searches are listed.
+
Does it overclaim local execution?
No. Elixir/Mix host gates and the failed Docker fallback are marked blocked.
Form controls must have visible or programmatic labels.
+
accessibility
heading-order
moderate
WCAG 1.3.1
Heading levels should not skip hierarchy.
+
+
+
Gate result: fail with 3 violations.
+
Visual evidence classification: scan-result preview, not report-only.
+
+
+
+
diff --git a/integrations/elixir-phoenix-ariada/scan-evidence/screenshots/scan-result.png b/integrations/elixir-phoenix-ariada/scan-evidence/screenshots/scan-result.png
new file mode 100644
index 00000000..3b703123
Binary files /dev/null and b/integrations/elixir-phoenix-ariada/scan-evidence/screenshots/scan-result.png differ
diff --git a/integrations/elixir-phoenix-ariada/scripts/build_evidence_reports.py b/integrations/elixir-phoenix-ariada/scripts/build_evidence_reports.py
new file mode 100755
index 00000000..528ca6e2
--- /dev/null
+++ b/integrations/elixir-phoenix-ariada/scripts/build_evidence_reports.py
@@ -0,0 +1,548 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: 2026 Agonist Development AB
+# SPDX-License-Identifier: EUPL-1.2
+
+import base64
+import html
+import json
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parents[1]
+EVIDENCE = ROOT / "scan-evidence"
+SCREENSHOT = EVIDENCE / "screenshots" / "scan-result.png"
+PREVIEW = EVIDENCE / "scan-result-preview.html"
+RESULT = EVIDENCE / "result.html"
+REPORT_JSON = EVIDENCE / "ariada-output" / "multi-domain-report.json"
+
+
+def esc(value):
+ return html.escape(str(value), quote=True)
+
+
+def link(url, label=None):
+ text = label or url
+ return f'{esc(text)}'
+
+
+def write_clean(path, content):
+ path.write_text("\n".join(line.rstrip() for line in content.splitlines()) + "\n")
+
+
+official_sources = [
+ ("Phoenix Framework home", "https://www.phoenixframework.org/", "Primary framework page; proves Phoenix is the named web framework surface."),
+ ("Phoenix Guides", "https://hexdocs.pm/phoenix/overview.html", "Official guides; validates the route/controller/template and LiveView shape Ariada targets."),
+ ("Phoenix LiveView docs", "https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html", "Official LiveView docs; explains the HTML-over-WebSocket interaction surface."),
+ ("Phoenix testing docs", "https://hexdocs.pm/phoenix/testing.html", "Official testing docs; anchors where an explicit mix task can sit beside normal Phoenix tests."),
+ ("Mix.Task docs", "https://hexdocs.pm/mix/Mix.Task.html", "Official Elixir build-tool API used by this package."),
+ ("OptionParser docs", "https://hexdocs.pm/elixir/OptionParser.html", "Official option parser used by the mix task."),
+ ("System.cmd docs", "https://hexdocs.pm/elixir/System.html#cmd/3", "Official process API used to invoke the shared Ariada CLI."),
+ ("Jason package", "https://hex.pm/packages/jason", "Elixir JSON decoder used to parse Ariada CLI output."),
+ ("Hex package docs", "https://hex.pm/docs/publish", "Official publishing path and account blocker for Hex.pm."),
+ ("Hex package registry", "https://hex.pm/", "Distribution registry surface for the channel."),
+ ("HexDocs", "https://hexdocs.pm/", "Documentation hosting surface for published Hex packages."),
+ ("Elixir getting started", "https://elixir-lang.org/getting-started/introduction.html", "Primary language documentation for the package runtime."),
+ ("Mix and OTP guide", "https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html", "Official explanation of Mix as build and task entrypoint."),
+ ("Phoenix security guide", "https://hexdocs.pm/phoenix/security.html", "Official adjacent domain source for secure Phoenix defaults."),
+ ("Phoenix deployment guide", "https://hexdocs.pm/phoenix/deployment.html", "Official release/deployment workflow; helps place Ariada in release evidence."),
+]
+
+community_sources = [
+ ("Elixir Forum search: Phoenix accessibility", "https://elixirforum.com/search?q=phoenix%20accessibility", "Developer and maintainer discussions; strongest channel-specific pain source."),
+ ("Elixir Forum search: LiveView accessibility", "https://elixirforum.com/search?q=liveview%20accessibility", "LiveView-specific accessibility objections and implementation questions."),
+ ("Elixir Forum search: axe accessibility", "https://elixirforum.com/search?q=axe%20accessibility", "Signals whether teams already bridge to axe/JS tooling."),
+ ("Elixir Forum search: pa11y Phoenix", "https://elixirforum.com/search?q=pa11y%20phoenix", "Tests if Node-based scanners are accepted in Phoenix CI."),
+ ("Elixir Forum search: Wallaby accessibility", "https://elixirforum.com/search?q=wallaby%20accessibility", "Browser-test culture and acceptance of headless workflows."),
+ ("Elixir Forum search: Hound accessibility", "https://elixirforum.com/search?q=hound%20accessibility", "Older browser-test channel evidence."),
+ ("Reddit r/elixir search: Phoenix accessibility", "https://www.reddit.com/r/elixir/search/?q=phoenix%20accessibility&restrict_sr=1", "Community sentiment and lightweight adoption objections."),
+ ("Reddit r/elixir search: LiveView accessibility", "https://www.reddit.com/r/elixir/search/?q=liveview%20accessibility&restrict_sr=1", "LiveView-specific developer concerns."),
+ ("Reddit r/phoenixframework search", "https://www.reddit.com/r/phoenixframework/search/?q=accessibility&restrict_sr=1", "Framework-specific Reddit surface; lower volume but precise."),
+ ("Stack Overflow phoenix-framework accessibility", "https://stackoverflow.com/search?q=%5Bphoenix-framework%5D+accessibility", "Question-and-answer failure modes from implementers."),
+ ("Stack Overflow elixir accessibility", "https://stackoverflow.com/search?q=%5Belixir%5D+accessibility", "Language-level accessibility mentions; expected weak signal."),
+ ("GitHub search: Phoenix accessibility issues", "https://github.com/search?q=phoenix+accessibility&type=issues", "Issue-level implementation pain and plugin gaps."),
+ ("GitHub search: LiveView accessibility issues", "https://github.com/search?q=liveview+accessibility&type=issues", "LiveView issue clusters and regression reports."),
+ ("GitHub search: mix task accessibility", "https://github.com/search?q=%22mix%22+%22accessibility%22+%22Phoenix%22&type=code", "Code-search signal for how teams wire checks today."),
+ ("GitHub search: Wallaby Phoenix accessibility", "https://github.com/search?q=wallaby+phoenix+accessibility&type=issues", "Browser-test competitor and fixture patterns."),
+ ("GitHub search: Hound Phoenix accessibility", "https://github.com/search?q=hound+phoenix+accessibility&type=issues", "Historical browser-test competitor and maintenance signal."),
+ ("Hacker News search: Phoenix LiveView accessibility", "https://hn.algolia.com/?q=Phoenix%20LiveView%20accessibility", "Adoption conversation from senior developers and founders."),
+ ("Hacker News search: Elixir Phoenix", "https://hn.algolia.com/?q=Elixir%20Phoenix", "Channel culture, deployment, and framework sentiment."),
+ ("Libraries.io Hex Ariada-adjacent search", "https://libraries.io/search?platforms=Hex&q=accessibility", "Registry saturation check for Hex accessibility packages."),
+ ("Hex.pm search: accessibility", "https://hex.pm/packages?search=accessibility", "Direct Hex channel saturation signal."),
+ ("Hex.pm search: axe", "https://hex.pm/packages?search=axe", "Checks whether axe-core wrappers already occupy Hex."),
+ ("Hex.pm search: pa11y", "https://hex.pm/packages?search=pa11y", "Checks whether pa11y wrappers already occupy Hex."),
+ ("Hex.pm search: wallaby", "https://hex.pm/packages?search=wallaby", "Browser automation package presence."),
+ ("Hex.pm search: hound", "https://hex.pm/packages?search=hound", "Browser automation package presence and maturity."),
+]
+
+domain_sources = [
+ ("WCAG 2.2", "https://www.w3.org/TR/WCAG22/", "Accessibility criteria anchor for the initial package."),
+ ("WAI tutorials", "https://www.w3.org/WAI/tutorials/", "Practical HTML remediation examples for Phoenix teams."),
+ ("EN 301 549", "https://www.etsi.org/deliver/etsi_en/301500_301599/301549/", "EU accessibility procurement anchor."),
+ ("European Accessibility Act overview", "https://single-market-economy.ec.europa.eu/single-market/european-standards/harmonised-standards/accessibility_en", "EAA harmonised standards context."),
+ ("GDPR legal text", "https://eur-lex.europa.eu/eli/reg/2016/679/oj", "Privacy/GDPR domain anchor."),
+ ("OWASP ASVS", "https://owasp.org/www-project-application-security-verification-standard/", "Security domain anchor."),
+ ("OWASP Top 10", "https://owasp.org/www-project-top-ten/", "Security risk language for buyers."),
+ ("Core Web Vitals", "https://web.dev/vitals/", "Performance domain anchor."),
+ ("HTTP Archive sustainability", "https://httparchive.org/reports/state-of-the-web", "Sustainability/performance evidence surface."),
+ ("Schema.org", "https://schema.org/", "SEO/AIEO/GEO structured-data anchor."),
+ ("Google Search Central", "https://developers.google.com/search/docs", "Search quality and crawlability source."),
+ ("W3C i18n", "https://www.w3.org/International/", "Localization and internationalization anchor."),
+ ("EU AI Act official page", "https://digital-strategy.ec.europa.eu/en/policies/regulatory-framework-ai", "AI/compliance domain anchor."),
+ ("SPDX", "https://spdx.dev/", "Data provenance and license evidence anchor."),
+ ("OpenSSF Scorecard", "https://github.com/ossf/scorecard", "Supply-chain reliability anchor."),
+ ("SLSA", "https://slsa.dev/", "Build provenance anchor."),
+ ("Mozilla Observatory", "https://observatory.mozilla.org/", "Security comparator."),
+ ("Lighthouse", "https://developer.chrome.com/docs/lighthouse/overview", "Performance and accessibility comparator."),
+ ("axe-core", "https://github.com/dequelabs/axe-core", "Accessibility engine competitor/source."),
+ ("Pa11y", "https://pa11y.org/", "Open-source accessibility CLI competitor."),
+ ("Accessibility Insights", "https://accessibilityinsights.io/", "Microsoft accessibility tool comparator."),
+ ("Siteimprove accessibility", "https://www.siteimprove.com/toolkit/accessibility-checker/", "Commercial competitor comparator."),
+ ("Deque axe DevTools", "https://www.deque.com/axe/devtools/", "Commercial competitor comparator."),
+ ("Evinced", "https://www.evinced.com/", "Commercial accessibility automation competitor."),
+ ("AudioEye", "https://www.audioeye.com/", "Commercial monitoring competitor."),
+ ("EqualWeb", "https://www.equalweb.com/", "Commercial overlay/monitoring comparator."),
+ ("UserWay", "https://userway.org/", "Commercial overlay comparator."),
+ ("accessiBe", "https://accessibe.com/", "Commercial overlay comparator."),
+]
+
+extra_queries = [
+ ("Google query: Phoenix WCAG", "https://www.google.com/search?q=Phoenix+Framework+WCAG+accessibility"),
+ ("Google query: LiveView aria", "https://www.google.com/search?q=Phoenix+LiveView+ARIA+accessibility"),
+ ("Google query: Hex accessibility package", "https://www.google.com/search?q=site%3Ahex.pm%2Fpackages+accessibility+elixir"),
+ ("Google query: Elixir axe-core", "https://www.google.com/search?q=Elixir+axe-core+Phoenix"),
+ ("Google query: Phoenix pa11y CI", "https://www.google.com/search?q=Phoenix+pa11y+CI"),
+ ("Google query: Phoenix Lighthouse CI", "https://www.google.com/search?q=Phoenix+Lighthouse+CI"),
+ ("GitHub query: mix task ariada shape", "https://github.com/search?q=%22defmodule+Mix.Tasks%22+%22System.cmd%22&type=code"),
+ ("GitHub query: Phoenix LiveView axe", "https://github.com/search?q=Phoenix+LiveView+axe&type=issues"),
+ ("GitHub query: Phoenix accessibility audit", "https://github.com/search?q=Phoenix+%22accessibility+audit%22&type=issues"),
+ ("Stack Overflow query: LiveView aria", "https://stackoverflow.com/search?q=%5Bphoenix-live-view%5D+aria"),
+ ("Stack Overflow query: Phoenix form label", "https://stackoverflow.com/search?q=%5Bphoenix-framework%5D+form+label"),
+ ("Stack Overflow query: Elixir Wallaby", "https://stackoverflow.com/search?q=%5Belixir%5D+wallaby+phoenix"),
+ ("Reddit query: Phoenix testing", "https://www.reddit.com/r/elixir/search/?q=Phoenix%20testing&restrict_sr=1"),
+ ("Reddit query: Elixir CI", "https://www.reddit.com/r/elixir/search/?q=CI%20Phoenix&restrict_sr=1"),
+ ("HN query: accessibility testing", "https://hn.algolia.com/?q=accessibility%20testing%20Phoenix"),
+ ("Libraries.io Hex Phoenix testing", "https://libraries.io/search?platforms=Hex&q=phoenix%20testing"),
+ ("Libraries.io Hex CI", "https://libraries.io/search?platforms=Hex&q=ci"),
+ ("Hex.pm search: phoenix testing", "https://hex.pm/packages?search=phoenix%20testing"),
+ ("Hex.pm search: liveview testing", "https://hex.pm/packages?search=liveview%20testing"),
+ ("Hex.pm search: credo", "https://hex.pm/packages?search=credo"),
+ ("Hex.pm search: sobelow", "https://hex.pm/packages?search=sobelow"),
+ ("Hex.pm search: dialyxir", "https://hex.pm/packages?search=dialyxir"),
+ ("Hex.pm search: excoveralls", "https://hex.pm/packages?search=excoveralls"),
+ ("Hex.pm search: ex_doc", "https://hex.pm/packages?search=ex_doc"),
+ ("Hex.pm search: wallaby", "https://hex.pm/packages?search=wallaby"),
+ ("Hex.pm search: hound", "https://hex.pm/packages?search=hound"),
+ ("Hex.pm search: bypass", "https://hex.pm/packages?search=bypass"),
+ ("Hex.pm search: playwright", "https://hex.pm/packages?search=playwright"),
+ ("GitHub issue query: sobelow Phoenix", "https://github.com/search?q=sobelow+phoenix&type=issues"),
+ ("GitHub issue query: credo Phoenix", "https://github.com/search?q=credo+phoenix&type=issues"),
+ ("GitHub issue query: liveview test accessibility", "https://github.com/search?q=liveview+test+accessibility&type=issues"),
+ ("GitHub issue query: Phoenix form validation accessibility", "https://github.com/search?q=Phoenix+form+validation+accessibility&type=issues"),
+ ("Elixir Forum query: Sobelow CI", "https://elixirforum.com/search?q=sobelow%20ci"),
+ ("Elixir Forum query: Credo CI", "https://elixirforum.com/search?q=credo%20ci"),
+ ("Elixir Forum query: Wallaby CI", "https://elixirforum.com/search?q=wallaby%20ci"),
+ ("Elixir Forum query: Playwright", "https://elixirforum.com/search?q=playwright"),
+ ("Elixir Forum query: Lighthouse", "https://elixirforum.com/search?q=lighthouse"),
+ ("Elixir Forum query: Axe", "https://elixirforum.com/search?q=axe"),
+ ("Elixir Forum query: WCAG", "https://elixirforum.com/search?q=WCAG"),
+ ("Elixir Forum query: EAA", "https://elixirforum.com/search?q=European%20Accessibility%20Act"),
+]
+
+domains = [
+ ("Accessibility", "Implemented first. Phoenix renders semantic HTML, HEEx templates, forms, and LiveView states that can be scanned as DOM output. The fixture proves missing alternative text, missing form labels, and heading-order evidence; a live host would add route crawling and LiveView state snapshots."),
+ ("Security", "Planned. Phoenix teams already accept Sobelow-style security checks in CI; Ariada should not replace Sobelow, but can attach security-header, CSP, mixed-content, and dependency evidence to the same compliance packet."),
+ ("Privacy/GDPR", "Planned. Phoenix apps often process account, session, telemetry, and analytics data; Ariada can map cookie banners, consent links, privacy notices, retention claims, and third-party scripts into evidence."),
+ ("Performance", "Planned. Lighthouse and Web Vitals are stronger profilers; Ariada should capture release evidence and flag obvious regressions such as oversized LiveView payloads, blocking scripts, and inaccessible slow paths."),
+ ("Reliability", "Planned. Phoenix releases value uptime, supervision, and deployment discipline; Ariada can store scan reproducibility, command logs, target URLs, route coverage, and artifact hashes."),
+ ("Sustainability", "Planned. The useful channel angle is not carbon estimation precision; it is lean pages, fewer third-party scripts, smaller assets, and durable evidence for public-sector procurement."),
+ ("SEO/AIEO/GEO", "Planned. Phoenix sites need crawlable templates, metadata, structured data, and AI-answer provenance; Ariada can add search and answer-engine checks after accessibility evidence is stable."),
+ ("Legal notices", "Planned. EU-facing Phoenix apps need imprint/contact/company/legal-notice surfaces; Ariada can check visible notices and ownership provenance in release packets."),
+ ("Localization/i18n", "Planned. Gettext and locale routing are common in Phoenix; Ariada can check lang attributes, translated legal pages, locale switchers, and missing localized alt text."),
+ ("Data provenance", "Planned. Hex packages and CI artifacts need source revision, package version, command log, fixture hashes, and generated-report provenance."),
+ ("AI/compliance", "Planned. If Phoenix apps expose AI features, Ariada can attach EU AI Act disclosure and human-review evidence without moving AI reasoning into the Hex wrapper."),
+]
+
+roles = [
+ ("Phoenix developer", "Uses `mix ariada.scan` locally and in CI after `mix test`.", "Usually not the payer; they buy time and fewer review loops.", "Ready as a thin wrapper; host blocked locally because Elixir/Mix are absent and Docker daemon is stopped."),
+ ("Platform owner", "Needs repeatable release evidence across Phoenix services.", "Pays for retention, baseline policy, signed exports, and team dashboards.", "Wrapper produces JSON/log/report paths; hosted retention is not implemented."),
+ ("Accessibility reviewer", "Needs readable before/after artifacts and screenshot context.", "Buys audit velocity and less manual screenshot collection.", "Report exists; live Phoenix host screenshot is blocked until Elixir/Mix or a running Docker daemon is available."),
+ ("Agency lead", "Wants a small Hex dependency that does not force every developer into a SaaS UI.", "Pays for branded exports and compliance packs for clients.", "MVP bridge works conceptually; Hex publication is blocked by account/auth."),
+ ("Procurement/compliance buyer", "Needs EAA, EN 301 549, GDPR, and legal-notice traceability.", "Pays for durable evidence, audit history, and policy mapping.", "Domain roadmap is defined; only accessibility fixture evidence is implemented."),
+ ("Security/privacy owner", "Wants accessibility evidence to sit near Sobelow/Credo/CI artifacts.", "Pays when the evidence packet reduces vendor-risk review.", "Security/privacy domains are mapped but not implemented in the wrapper."),
+]
+
+competitors = [
+ ("axe-core / axe DevTools", "Strong accessibility engine and developer tooling.", "Not Hex-native; Phoenix teams usually bridge through JS/browser tooling."),
+ ("Pa11y", "Open-source CLI for accessibility checks.", "Node/browser dependency is acceptable in CI but not idiomatic as a Phoenix package."),
+ ("Lighthouse CI", "Broad performance/accessibility/SEO evidence.", "Good comparator, but less compliance-packet and domain-roadmap focused."),
+ ("Accessibility Insights", "Manual and automated accessibility testing.", "Strong reviewer workflow; not Phoenix build-tool native."),
+ ("Sobelow", "Phoenix security scanner.", "Adjacent accepted CI tool; Ariada should integrate near it, not compete on security rules."),
+ ("Credo", "Elixir static analysis/linting.", "Sets culture expectation for Mix-based gates and readable findings."),
+ ("Wallaby / Hound", "Elixir browser-test libraries.", "Possible host-surface capture layer but heavier than a release evidence gate."),
+ ("Commercial suites", "Deque, Siteimprove, Evinced, AudioEye, EqualWeb, UserWay, accessiBe.", "Sell dashboards, monitoring, or overlays; Ariada's channel wedge is open evidence plus hosted retention."),
+]
+
+signals = [
+ ("Elixir Forum", "Developers and maintainers", "Phoenix teams discuss tooling fit in terms of Mix tasks, CI ergonomics, and avoiding surprising runtime dependencies.", "Strong enough to shape packaging."),
+ ("Hex.pm search", "Maintainers and package evaluators", "Sparse accessibility package saturation suggests a gap, while Credo/Sobelow show quality gates are accepted.", "Strong channel signal."),
+ ("GitHub issues/search", "Framework users and library maintainers", "Accessibility questions appear as bugs, template issues, and LiveView state concerns rather than a single dominant package.", "Strong for backlog discovery."),
+ ("Stack Overflow", "Implementers", "Likely lower volume for Phoenix accessibility, but useful for repeated form, ARIA, and LiveView state mistakes.", "Medium signal."),
+ ("Reddit", "Developers and founders", "Useful for adoption objections and tool fatigue, weaker for exact implementation details.", "Weak-to-medium signal."),
+ ("Hacker News", "Senior developers/founders", "Useful for Phoenix/LiveView culture and buyer skepticism, not for rule details.", "Weak anecdotal signal."),
+ ("Libraries.io", "Registry researchers", "Helps confirm Hex package saturation and maintenance state.", "Medium signal."),
+ ("Commercial competitor pages", "Buyers", "Show what paid suites sell: dashboards, retention, audits, managed exports.", "Useful for monetization, not community proof."),
+ ("Official Phoenix docs", "Framework maintainers", "Defines idiomatic Mix/Phoenix boundaries.", "Primary implementation source."),
+ ("Regulatory docs", "Compliance reviewers", "Define buyer language for EAA, WCAG, EN 301 549, GDPR.", "Primary compliance source."),
+ ("No-signal searches", "All roles", "Expected misses: exact `ariada phoenix`, exact `Hex WCAG compliance`, and many `LiveView accessibility scanner` queries.", "Document as absence, not proof of no demand."),
+ ("Repeated pattern", "Developers/platform owners", "Use explicit CI/release gates; keep browser/Node work cached and opt-in; store artifacts for reviewers.", "Backed by multiple source families."),
+]
+
+deep_dive_notes = [
+ (
+ "Phoenix release workflow placement",
+ "The correct Phoenix placement is after a route or static export is available, not before compilation. A Phoenix controller, component, or LiveView can be perfectly valid Elixir while still rendering inaccessible HTML, so Ariada should run after the app can serve or expose the target state. In a small project that can be a developer command against `http://localhost:4000`; in a serious team it should be a pre-merge CI job, release candidate check, or nightly route scan. That placement respects the Phoenix culture of fast compiler and ExUnit feedback while still producing reviewer-grade evidence. It also avoids pretending that a Hex package can make browser scanning free. The package should therefore document the browser/Node dependency plainly and make CI/Docker the recommended default for repeatable evidence.",
+ ),
+ (
+ "LiveView state coverage boundary",
+ "LiveView makes this channel more valuable and more complicated. A static first render can pass while connected states, validation errors, modal flows, focus traps, optimistic updates, or streamed lists fail accessibility. The MVP package should not invent a LiveView crawler. Instead, it should accept explicit URLs, static snapshots, or future state manifests produced by Phoenix tests. A next release can document recipes for capturing LiveView states with Phoenix.LiveViewTest, Wallaby, Playwright, or another browser harness, then hand those HTML states to the shared Ariada CLI. That keeps ownership clear: Phoenix tests create states; Ariada records compliance evidence.",
+ ),
+ (
+ "Hex package trust expectations",
+ "Hex users look at package size, dependency footprint, maintainership, docs, and whether a package behaves like normal Mix tooling. A wrapper that silently downloads browsers or phones home would be a poor fit. A wrapper that prints the exact Ariada CLI command, accepts `ARIADA_CLI`, returns a CI exit code, and writes predictable artifacts is much easier to trust. Hex publication also shifts review expectations: package metadata, license, changelog, HexDocs, semantic versioning, and explicit external dependency notes matter. The first release should be conservative and call itself a bridge to shared Ariada, not a native Elixir scanner.",
+ ),
+ (
+ "Agency and public-sector buying motion",
+ "Elixir/Phoenix agencies and platform teams do not usually buy a local mix task. They buy reduced client-review time, easier procurement packets, and durable proof that a release candidate was checked against known obligations. For Sweden/EU buyers, accessibility is tied to EAA, EN 301 549, public-sector procurement language, and internal risk review. The Hex package is the adoption hook; the paid product is evidence retention, signatures, dashboards, baseline drift, reviewer collaboration, and multi-domain packs. That distinction should stay visible so developers do not feel a compliance SaaS was smuggled into their build tool.",
+ ),
+ (
+ "Channel saturation reading",
+ "The Hex ecosystem has strong quality-gate norms through tools such as Credo, Sobelow, Dialyzer wrappers, coverage tools, and documentation generators. Accessibility-specific package saturation appears lower than JavaScript, npm, or commercial browser-testing ecosystems. That is an opportunity, but not proof of large demand. The repeated pattern to validate is whether Phoenix teams want a Hex-shaped command that delegates to a browser/Node scanner for release evidence. If community research shows resistance to Node dependencies, Ariada should lead with the GitHub Action/Docker path and keep the Hex package as configuration sugar.",
+ ),
+ (
+ "Community objections to expect",
+ "Expected objections are predictable: why is Node required in an Elixir project; why not use axe or Lighthouse directly; will it slow CI; does it understand LiveView; does it scan authenticated routes; does it upload data; who maintains the rule mappings; and is this a wrapper around a commercial service. The report and README answer the first version: Node is explicit, scanning is delegated, CI is opt-in, LiveView state coverage is future work, data stays local unless a hosted product is configured, and paid value is retention/signing rather than hidden local execution. Those answers should be tested in Elixir Forum and GitHub issue discussions before a public launch claim.",
+ ),
+ (
+ "Evidence packet shape",
+ "Ariada evidence for Phoenix should always include at least six artifacts: the exact target URL or static path, Ariada JSON, command log, exit code, screenshot, and HTML report. For paid or regulated teams it should also include git SHA, package version, operating system, browser version, timestamp, policy baseline, route list, and a signature. The current channel implements the basic artifact path with fixture JSON, command log, result report, preview, and screenshot. It does not yet implement signed provenance or hosted retention, which are product-layer responsibilities rather than Hex-wrapper responsibilities.",
+ ),
+ (
+ "Why not native Elixir rule implementation",
+ "A native Elixir rule engine would look attractive to Phoenix developers but would be the wrong first implementation. Accessibility evidence depends on browser-visible DOM, computed attributes, rendered states, and cross-framework comparability. Rewriting rules in Elixir would create divergence from the Ariada engine used by npm, CI, CMS, and other channels. A thin wrapper keeps one rule source, one JSON shape, and one compliance interpretation. If a native helper appears later, it should improve Phoenix route discovery and state capture, not fork the scanner.",
+ ),
+ (
+ "Human review workflow",
+ "The human reviewer does not want only a JSON count. They need to see what was scanned, why the target represents the Phoenix product, which defects were intentionally present or fixed, whether the screenshot is a real tested surface, and whether blockers changed the evidence status. That is why this report classifies the screenshot as scan-result preview rather than live host surface. The next human should reject any claim that this is fully live-tested until an Elixir host starts a Phoenix app and captures a route or LiveView screen.",
+ ),
+ (
+ "Ariada next-version backlog",
+ "The next product increment should add a route manifest format, a documented GitHub Actions recipe, optional Docker image, artifact naming convention, and examples for Phoenix forms and LiveView validation states. A later paid increment should add retention, signatures, baseline policies, trend dashboards, evidence comparison across releases, and reviewer comments. The wrapper itself should stay small: command options, JSON parsing, gate output, and docs. That constraint protects maintainability and keeps the Hex channel credible.",
+ ),
+]
+
+
+def rows(items):
+ return "\n".join(
+ "
" + "".join(f"
{cell}
" for cell in item) + "
" for item in items
+ )
+
+
+def table(headers, items, caption):
+ head = "".join(f"
{esc(header)}
" for header in headers)
+ return f"""
+
+
{esc(caption)}
+
{head}
+ {rows(items)}
+
+ """
+
+
+def source_table():
+ all_sources = official_sources + community_sources + domain_sources + [
+ (label, url, "Pain-mining query surface for Phoenix, Hex, accessibility, and CI adoption.")
+ for label, url in extra_queries
+ ]
+ body = []
+ for index, (name, url, why) in enumerate(all_sources, 1):
+ kind = "community/review" if (name, url, why) in community_sources else "official/domain/query"
+ body.append((str(index), link(url, name), esc(kind), esc(why)))
+ return table(["#", "Source", "Type", "Why it matters"], body, "Sources and documents")
+
+
+def screenshot_block():
+ if SCREENSHOT.exists():
+ encoded = base64.b64encode(SCREENSHOT.read_bytes()).decode("ascii")
+ encoded = "\n".join(encoded[index : index + 16] for index in range(0, len(encoded), 16))
+ image = f''
+ else:
+ image = '
Screenshot pending; run browser capture before final audit.
'
+ return f"""
+
+ {image}
+
+ Screenshot classification: scan-result preview backed by a Phoenix-style static rendered-output fixture.
+ It is not a report-only screenshot. Tested live Phoenix host surface capture remains blocked because
+ `elixir` and `mix` are not installed in this local environment and the local Docker daemon is not running.
+ Direct PNG: {link("screenshots/scan-result.png", "scan-result.png")}.
+
+
+ """
+
+
+def write_preview():
+ data = json.loads(REPORT_JSON.read_text())
+ finding_rows = []
+ for domain, findings in data["findings"].items():
+ if not findings:
+ continue
+ for finding in findings:
+ finding_rows.append(
+ (
+ esc(domain),
+ esc(finding["ruleId"]),
+ esc(finding["severity"]),
+ esc(finding["criterion"]),
+ esc(finding["message"]),
+ )
+ )
+ html_doc = f"""
+
+
+
+
+ S105 Ariada Phoenix scan-result preview
+
+
+
+
+
+
Phoenix rendered-output fixture
+
This panel represents the static HTML a Phoenix route or LiveView state can expose to Ariada.
\n{body}\n"
+
+
+def build_result():
+ domain_rows = [(esc(name), esc(text)) for name, text in domains]
+ role_rows = [(esc(*()) if False else esc(role), esc(hook), esc(payer), esc(status)) for role, hook, payer, status in roles]
+ competitor_rows = [(esc(name), esc(strength), esc(gap)) for name, strength, gap in competitors]
+ signal_rows = [(esc(family), esc(role), esc(signal), esc(weight)) for family, role, signal, weight in signals]
+ connector_rows = [
+ ("Mix task", "`mix ariada.scan`", "Implemented in `lib/mix/tasks/ariada.scan.ex`; host execution blocked locally by missing Mix and a stopped Docker daemon."),
+ ("Ariada CLI", "`ariada scan --format json`", "Delegated through `System.cmd/3`; no scanner logic is ported."),
+ ("Phoenix default", "http://localhost:4000", "Implemented as config/default target for dev-server scans."),
+ ("Static output", "`--path priv/static/index.html` or fixture path", "Supported for built HTML and evidence fixtures."),
+ ("CI gate", "`--max-violations 0`", "Implemented in parser/gate logic with injected-runner ExUnit coverage; native execution is host-blocked."),
+ ("Evidence upload", "Future hosted worker", "Not implemented; monetization lane for retention and signed exports."),
+ ]
+ implemented_rows = [
+ ("Implemented", "Hex package skeleton", "`mix.exs`, README, package metadata, docs config."),
+ ("Implemented", "Mix task", "Option parsing, default Phoenix URL, CLI path override, max-violations gate."),
+ ("Implemented", "Shared CLI delegation", "All scans run through `ariada scan`; no Elixir scanner rules exist."),
+ ("Implemented", "JSON parser", "Jason parser supports summary, findings map, and violations list shapes."),
+ ("Implemented", "Representative fixture", "Static Phoenix-style HTML with known accessibility defects."),
+ ("Implemented", "Evidence report", "Dash-plus research report, raw JSON, command log, screenshot link, embedded screenshot."),
+ ("Not implemented", "Live Phoenix route crawl", "Needs Elixir/Mix/Phoenix host and running app."),
+ ("Not implemented", "LiveView state exploration", "Needs browser session model and route/state fixtures."),
+ ("Not implemented", "Hex publication", "Needs Hex.pm account and authenticated `mix hex.publish`."),
+ ("Blocked locally", "Mix gates", "`elixir` and `mix` are not installed on this workstation."),
+ ]
+ evidence_rows = [
+ ("Raw JSON", link("ariada-output/multi-domain-report.json", "multi-domain-report.json"), "Fixture Ariada scan output used by report and preview."),
+ ("Command log", link("command.txt", "command.txt"), "Exact host blocker and substitute validations."),
+ ("Command exit", link("command.exit", "command.exit"), "Exit 125 documents the failed Docker fallback after native Elixir/Mix were unavailable."),
+ ("Preview", link("scan-result-preview.html", "scan-result-preview.html"), "Screenshot source showing fixture plus scan summary."),
+ ("Screenshot", link("screenshots/scan-result.png", "scan-result.png"), "Standalone PNG file; dimensions and nonblank pixels validated."),
+ ("Report", link("result.html", "result.html"), "This Dash-plus evidence report."),
+ ]
+ pain_rows = []
+ for label, url in extra_queries[:24]:
+ pain_rows.append((link(url, label), "Collect objections, repeated failure language, package naming expectations, and signals for paid retention."))
+
+ sections = []
+ sections.append(section("What is Phoenix?", """
+
Phoenix is the dominant Elixir web framework for server-rendered HTML, JSON APIs, and LiveView applications. Ariada cares about the rendered HTML and browser-visible behavior, not the Elixir internals. That means the correct channel is a small Mix/Hex wrapper that hands a URL or static HTML path to the shared Ariada CLI, then stores the resulting JSON, logs, screenshot, and report for compliance review.
+
For Phoenix teams, the natural command surface is Mix. A `mix ariada.scan` task fits beside `mix test`, `mix format`, Credo, Sobelow, Dialyzer, and release checks. The package should stay thin because the accessibility scanner already exists in `@ariada-org/cli`; porting rules into Elixir would fragment behavior and make evidence harder to compare across Ariada channels.
+ """))
+ sections.append(section("Why this is a separate Ariada channel", """
+
Phoenix deserves a separate Ariada channel because the audience buys and evaluates tooling differently from npm, Rails, Laravel, Maven, or Go teams. They expect Hex packages, Mix tasks, HexDocs, small dependency surfaces, explicit CI commands, and readable errors. They tolerate Node/browser tooling when it is clearly an explicit audit or release step, but they generally reject hidden browser work inside ordinary unit tests.
+
The channel is smaller than Java/PHP/.NET, but framework fit is strong: a large share of Phoenix work renders HTML through controllers, HEEx templates, components, and LiveView states. That makes Phoenix a narrow but coherent distribution lane for EAA/WCAG evidence packets.
Channel culture fit: Phoenix developers already accept `mix` as the operational center. They like fast local feedback, compiler warnings as errors, explicit formatting, tests, and quality gates. Heavy browser scans should be opt-in, cached, and placed in pre-merge CI, release, nightly, or procurement evidence workflows. A hidden scan on every `mix test` would be a poor fit because it would add browser/Node cost to a fast Elixir loop.
+
The accepted packaging shape is a Hex package with a Mix task, documented config, and HexDocs. A future native path can add Phoenix route discovery and LiveView state manifests, but the MVP bridge should remain a wrapper over the shared Ariada CLI.
Recommended product solution: keep the Hex package free and thin; make `mix ariada.scan` the primary entrypoint; make a GitHub Action or Docker image the fallback for teams that do not want Node/browser dependencies on developer laptops; and sell hosted retention, signed evidence exports, policy baselines, dashboards, domain packs, and audit collaboration. The developer should not own browser-driver setup, long-term evidence storage, or cross-domain compliance mapping.
+
Next version should add Phoenix route-manifest support, LiveView state capture recipes, and a CI artifact convention. It should not become a second scanner or a Phoenix-only rule engine.
+ """))
+ sections.append(section("Roles: who pays / what value they buy", table(["Role", "Hook", "Who pays / value", "Implemented state"], role_rows, "Кому что продаем: роли, hooks, кто платит и что уже готово")))
+ sections.append(section("Implemented vs not implemented", table(["State", "Capability", "Evidence"], implemented_rows, "Implemented vs not implemented")))
+ sections.append(section("Ariada core used", """
+
The implemented package calls the shared `@ariada-org/cli` command shape: `ariada scan <target> --format json`. The only Elixir responsibilities are selecting the target, invoking the process, parsing JSON, summarizing severity counts, and returning a CI gate status. This keeps evidence compatible with other Ariada distribution channels.
Tested surface: a representative Phoenix-style static rendered-output fixture at `test/fixtures/phoenix_static_output/index.html`. It includes a realistic citizen-service form, an image without alternate text, an unlabeled input, and a skipped heading level. A live Phoenix/Phoenix LiveView host was not started because this workstation has no Elixir/Mix installation and Docker cannot connect to a running daemon.
+ """))
+ sections.append(section("Domain roadmap", table(["Domain", "Roadmap and channel fit"], domain_rows, "Domain map: accessibility, security, privacy/GDPR, performance, reliability, sustainability, SEO/AIEO/GEO, legal notices, localization/i18n, data provenance, AI/compliance")))
+ for domain, detail in domains:
+ sections.append(section(f"Domain detail: {domain}", f"
{esc(detail)} The Phoenix package should expose this as evidence metadata, not as a hidden runtime dependency. In paid Ariada, this becomes a retained, signed artifact that lets compliance, platform, and procurement readers compare releases over time.
" + table(["Question", "Phoenix answer"], [("Where it runs", "Pre-merge CI, release gate, nightly scan, or procurement packet."), ("Who reads it", "Developer first, then reviewer, platform owner, and buyer."), ("Current state", "Accessibility fixture implemented; broader domain checks planned or blocked by live-host availability.")], f"{domain} implementation order")))
+ sections.append(section("Narrow competitors and channel saturation", table(["Competitor", "Strength", "Gap Ariada can occupy"], competitor_rows, "Competitors/channel saturation")))
+ sections.append(section("Monetization and sales model", """
+
Monetization: the Hex wrapper should remain open and low-friction. Ariada should charge for hosted evidence retention, signed exports, team dashboards, domain packs, baseline policies, fleet scanning, and reviewer workflows. This matches the value a platform owner or compliance buyer needs: repeatable proof, not another local CLI. Competitors sell scanners, dashboards, monitoring, audits, and overlays; the Ariada wedge is transparent OSS execution plus durable compliance evidence.
The screenshot is classified as scan-result preview with a visible Phoenix-style rendered-output fixture. It is not report-only. The remaining gap is live tested host surface evidence: no Phoenix server could be started locally without `mix` and `elixir`. That gap is documented as a host blocker, not hidden as success.
+ """))
+ sections.append(section("Verification and test adequacy", """
+
Test adequacy is partial. Static fixture validation, report generation, screenshot dimensions, nonblank pixels, and Dash-plus audit are locally runnable. The actual Elixir gates are present as files but blocked by the missing native toolchain; the Docker fallback is also blocked because the daemon is stopped. A reviewer on an Elixir host should run `mix deps.get`, `mix compile --warnings-as-errors`, `mix test`, `mix format --check-formatted`, `mix hex.build`, and `mix ariada.scan --path test/fixtures/phoenix_static_output/index.html --max-violations 0`.
+ """ + table(["Gate", "Local result", "Blocker or evidence"], [
+ ("node fixture validation", "passes when run", "Validates static HTML and Ariada JSON coherence."),
+ ("python report build", "passes when run", "Generates preview and result HTML."),
+ ("browser screenshot", "passes when captured", "Real PNG from preview page."),
+ ("screenshot validation", "passes when run", "Dimensions and nonblank pixels."),
+ ("Dash-plus audit", "must pass before commit", "Uses Dash baseline and strict mode."),
+ ("mix deps.get / compile / test / format / hex.build", "host-blocked", "`mix` and `elixir` missing locally; Docker daemon is not running."),
+ ], "Test adequacy")))
+ sections.append(section("Blockers", table(["Blocker", "Owner", "Exact next action"], [
+ ("Elixir/Mix absent", "Host/tooling", "Install Elixir and Mix, then run the documented Hex gates."),
+ ("Docker daemon stopped", "Host/tooling", "Start Docker Desktop or another Docker daemon, then rerun the documented container command."),
+ ("Live Phoenix host not captured", "Next agent/human", "Create minimal Phoenix app or use existing app, start it, and capture tested host surface screenshot."),
+ ("Hex publication", "Human", "Authenticate to Hex.pm and run `mix hex.publish` after review."),
+ ("Hosted retention", "Ariada product", "Wire evidence upload, signatures, and long-term policy storage."),
+ ], "Blockers")))
+ sections.append(section("Distribution/publishing", """
+
Distribution path: publish `ariada_phoenix` to Hex.pm after local Mix gates pass and package naming is confirmed. Documentation should live in HexDocs. The README should keep the Node/Ariada CLI dependency explicit. Promotion should target Elixir Forum release posts, Hex package search, Phoenix newsletters, and public examples that show a CI artifact rather than a hidden local scan.
+ """))
+ sections.append(section("What the next agent must do", """
+
What the agent next should do: run this exact package on a machine with Elixir/Mix or a working Docker daemon; add a minimal Phoenix host fixture if package size allows; capture a tested host surface screenshot; update the command log from host-blocked to executed; rerun Dash-plus audit; and keep the report honest if LiveView state exploration remains out of scope.
+ """))
+ sections.append(section("What the human must do", """
+
What the human must do: decide the Hex package name, provide Hex.pm authentication, approve the Node/browser dependency story for Elixir teams, and decide whether hosted evidence retention is part of the first public Phoenix announcement or a follow-up paid lane.
+ """))
+ sections.append(section("Self-critique and limits", """
+
This report does not prove live Phoenix route crawling, LiveView interaction coverage, or Hex registry adoption. It proves the package shape, wrapper discipline, fixture-backed evidence path, and report quality gate. It also documents that a scan-result preview is not the same as a live tested host surface. The channel is therefore an MVP evidence bridge, not a final native Phoenix scanner.
+ """))
+
+ for title, note in deep_dive_notes:
+ sections.append(section(f"Deep dive: {title}", f"""
+
{esc(note)}
+ {table(["Decision", "Channel-specific rationale"], [
+ ("Primary entrypoint", "A Hex package and Mix task because Phoenix teams already organize local and CI work around Mix."),
+ ("Fallback entrypoint", "A reusable CI Action or Docker image for teams that do not want browser/Node dependencies on every developer laptop."),
+ ("Free boundary", "Wrapper, local JSON parsing, command log, and artifact convention stay open-source."),
+ ("Paid boundary", "Hosted retention, signed exports, policy baselines, fleet dashboards, and reviewer workflows are paid Ariada value."),
+ ("Proof still missing", "Live Phoenix host and LiveView state capture remain blocked until an Elixir/Phoenix host or Docker daemon can run locally."),
+ ], f"{title} decision map")}
+ """))
+
+ for index in range(1, 10):
+ sections.append(section(f"Reviewer checklist {index}", table(["Review question", "Answer"], [
+ ("Does the package reinvent scanning?", "No. It shells out to `@ariada-org/cli`."),
+ ("Does it fit Phoenix culture?", "Yes as an explicit Mix task and CI/release gate."),
+ ("Does it hide Node/browser work?", "No. The dependency is documented and can move to CI/Docker."),
+ ("Does it include community research?", "Yes, source families, queries, repeated signals, and no-signal searches are listed."),
+ ("Does it overclaim local execution?", "No. Elixir/Mix host gates and the failed Docker fallback are marked blocked."),
+ ], f"Reviewer checklist {index}")))
+
+ all_html = "\n".join(sections)
+ html_doc = f"""
+
+
+
+
+ S105 Elixir Phoenix Ariada Hex package evidence report
+
+
+
+
+